skedyul 1.1.9 → 1.2.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/esm/index.mjs +4255 -0
- package/package.json +2 -1
|
@@ -0,0 +1,4255 @@
|
|
|
1
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { z as z6 } from "zod/v4";
|
|
11
|
+
|
|
12
|
+
// src/types/invocation.ts
|
|
13
|
+
function createToolCallContext(params) {
|
|
14
|
+
return {
|
|
15
|
+
invocationId: params.invocationId,
|
|
16
|
+
invocationType: "tool_call",
|
|
17
|
+
toolCallId: params.toolCallId,
|
|
18
|
+
toolHandle: params.toolHandle,
|
|
19
|
+
appInstallationId: params.appInstallationId,
|
|
20
|
+
workflowId: params.workflowId,
|
|
21
|
+
workflowVersionId: params.workflowVersionId,
|
|
22
|
+
workflowRunId: params.workflowRunId
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function createServerHookContext(params) {
|
|
26
|
+
return {
|
|
27
|
+
invocationId: params.invocationId,
|
|
28
|
+
invocationType: "server_hook",
|
|
29
|
+
serverHookHandle: params.serverHookHandle,
|
|
30
|
+
appInstallationId: params.appInstallationId
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createWebhookContext(params) {
|
|
34
|
+
return {
|
|
35
|
+
invocationId: params.invocationId,
|
|
36
|
+
invocationType: "webhook",
|
|
37
|
+
appInstallationId: params.appInstallationId
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createWorkflowStepContext(params) {
|
|
41
|
+
return {
|
|
42
|
+
invocationId: params.invocationId,
|
|
43
|
+
invocationType: "workflow_step",
|
|
44
|
+
workflowId: params.workflowId,
|
|
45
|
+
workflowVersionId: params.workflowVersionId,
|
|
46
|
+
workflowRunId: params.workflowRunId,
|
|
47
|
+
workflowStepId: params.workflowStepId
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/types/tool-context.ts
|
|
52
|
+
function isProvisionContext(ctx) {
|
|
53
|
+
return ctx.trigger === "provision";
|
|
54
|
+
}
|
|
55
|
+
function isRuntimeContext(ctx) {
|
|
56
|
+
return ctx.trigger !== "provision";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/types/tool.ts
|
|
60
|
+
import { z } from "zod/v4";
|
|
61
|
+
var ToolResponseMetaSchema = z.object({
|
|
62
|
+
/** Whether the tool execution succeeded */
|
|
63
|
+
success: z.boolean(),
|
|
64
|
+
/** Human-readable message describing the result or error */
|
|
65
|
+
message: z.string(),
|
|
66
|
+
/** Name of the tool that was executed */
|
|
67
|
+
toolName: z.string()
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// src/types/webhook.ts
|
|
71
|
+
function isRuntimeWebhookContext(ctx) {
|
|
72
|
+
return "appInstallationId" in ctx && ctx.appInstallationId !== void 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/schemas.ts
|
|
76
|
+
import { z as z2 } from "zod/v4";
|
|
77
|
+
var EnvVisibilitySchema = z2.enum(["visible", "encrypted"]);
|
|
78
|
+
var EnvVariableDefinitionSchema = z2.object({
|
|
79
|
+
label: z2.string(),
|
|
80
|
+
required: z2.boolean().optional(),
|
|
81
|
+
visibility: EnvVisibilitySchema.optional(),
|
|
82
|
+
default: z2.string().optional(),
|
|
83
|
+
description: z2.string().optional(),
|
|
84
|
+
placeholder: z2.string().optional()
|
|
85
|
+
});
|
|
86
|
+
var EnvSchemaSchema = z2.record(z2.string(), EnvVariableDefinitionSchema);
|
|
87
|
+
var ComputeLayerTypeSchema = z2.enum(["serverless", "dedicated"]);
|
|
88
|
+
var FieldOwnerSchema = z2.enum(["APP", "SHARED"]);
|
|
89
|
+
var PrimitiveSchema = z2.union([z2.string(), z2.number(), z2.boolean()]);
|
|
90
|
+
var PrimitiveOrArraySchema = z2.union([PrimitiveSchema, z2.array(PrimitiveSchema)]);
|
|
91
|
+
var FilterOperatorSchema = z2.enum([
|
|
92
|
+
"eq",
|
|
93
|
+
"neq",
|
|
94
|
+
"gt",
|
|
95
|
+
"gte",
|
|
96
|
+
"lt",
|
|
97
|
+
"lte",
|
|
98
|
+
"in",
|
|
99
|
+
"contains",
|
|
100
|
+
"notContains",
|
|
101
|
+
"not_contains",
|
|
102
|
+
"startsWith",
|
|
103
|
+
"starts_with",
|
|
104
|
+
"endsWith",
|
|
105
|
+
"ends_with",
|
|
106
|
+
"isEmpty",
|
|
107
|
+
"isNotEmpty"
|
|
108
|
+
]);
|
|
109
|
+
var FilterConditionSchema = z2.record(
|
|
110
|
+
FilterOperatorSchema,
|
|
111
|
+
PrimitiveOrArraySchema
|
|
112
|
+
);
|
|
113
|
+
var StructuredFilterSchema = z2.record(
|
|
114
|
+
z2.string(),
|
|
115
|
+
FilterConditionSchema
|
|
116
|
+
);
|
|
117
|
+
var ModelDependencySchema = z2.object({
|
|
118
|
+
model: z2.string(),
|
|
119
|
+
fields: z2.array(z2.string()).optional(),
|
|
120
|
+
where: StructuredFilterSchema.optional()
|
|
121
|
+
});
|
|
122
|
+
var ChannelDependencySchema = z2.object({
|
|
123
|
+
channel: z2.string()
|
|
124
|
+
});
|
|
125
|
+
var WorkflowDependencySchema = z2.object({
|
|
126
|
+
workflow: z2.string()
|
|
127
|
+
});
|
|
128
|
+
var ResourceDependencySchema = z2.union([
|
|
129
|
+
ModelDependencySchema,
|
|
130
|
+
ChannelDependencySchema,
|
|
131
|
+
WorkflowDependencySchema
|
|
132
|
+
]);
|
|
133
|
+
var FieldDataTypeSchema = z2.enum([
|
|
134
|
+
"LONG_STRING",
|
|
135
|
+
"STRING",
|
|
136
|
+
"NUMBER",
|
|
137
|
+
"BOOLEAN",
|
|
138
|
+
"DATE",
|
|
139
|
+
"DATE_TIME",
|
|
140
|
+
"TIME",
|
|
141
|
+
"FILE",
|
|
142
|
+
"IMAGE",
|
|
143
|
+
"RELATION",
|
|
144
|
+
"OBJECT"
|
|
145
|
+
]);
|
|
146
|
+
var FieldOptionSchema = z2.object({
|
|
147
|
+
label: z2.string(),
|
|
148
|
+
value: z2.string(),
|
|
149
|
+
color: z2.string().optional()
|
|
150
|
+
});
|
|
151
|
+
var InlineFieldDefinitionSchema = z2.object({
|
|
152
|
+
limitChoices: z2.number().optional(),
|
|
153
|
+
options: z2.array(FieldOptionSchema).optional(),
|
|
154
|
+
minLength: z2.number().optional(),
|
|
155
|
+
maxLength: z2.number().optional(),
|
|
156
|
+
min: z2.number().optional(),
|
|
157
|
+
max: z2.number().optional(),
|
|
158
|
+
relatedModel: z2.string().optional(),
|
|
159
|
+
pattern: z2.string().optional()
|
|
160
|
+
});
|
|
161
|
+
var AppFieldVisibilitySchema = z2.object({
|
|
162
|
+
data: z2.boolean().optional(),
|
|
163
|
+
list: z2.boolean().optional(),
|
|
164
|
+
filters: z2.boolean().optional()
|
|
165
|
+
});
|
|
166
|
+
var ModelFieldDefinitionSchema = z2.object({
|
|
167
|
+
handle: z2.string(),
|
|
168
|
+
label: z2.string(),
|
|
169
|
+
type: FieldDataTypeSchema.optional(),
|
|
170
|
+
definition: z2.union([InlineFieldDefinitionSchema, z2.string()]).optional(),
|
|
171
|
+
required: z2.boolean().optional(),
|
|
172
|
+
unique: z2.boolean().optional(),
|
|
173
|
+
system: z2.boolean().optional(),
|
|
174
|
+
isList: z2.boolean().optional(),
|
|
175
|
+
defaultValue: z2.object({ value: z2.unknown() }).optional(),
|
|
176
|
+
description: z2.string().optional(),
|
|
177
|
+
visibility: AppFieldVisibilitySchema.optional(),
|
|
178
|
+
owner: FieldOwnerSchema.optional()
|
|
179
|
+
});
|
|
180
|
+
var ModelDefinitionSchema = z2.object({
|
|
181
|
+
handle: z2.string(),
|
|
182
|
+
name: z2.string(),
|
|
183
|
+
namePlural: z2.string().optional(),
|
|
184
|
+
labelTemplate: z2.string().optional(),
|
|
185
|
+
description: z2.string().optional(),
|
|
186
|
+
fields: z2.array(ModelFieldDefinitionSchema),
|
|
187
|
+
requires: z2.array(ResourceDependencySchema).optional(),
|
|
188
|
+
addDefaultPages: z2.boolean().optional(),
|
|
189
|
+
addNavigation: z2.boolean().optional()
|
|
190
|
+
});
|
|
191
|
+
var RelationshipCardinalitySchema = z2.enum([
|
|
192
|
+
"ONE_TO_ONE",
|
|
193
|
+
"ONE_TO_MANY"
|
|
194
|
+
]);
|
|
195
|
+
var OnDeleteBehaviorSchema = z2.enum(["NONE", "CASCADE", "RESTRICT"]);
|
|
196
|
+
var RelationshipLinkSchema = z2.object({
|
|
197
|
+
model: z2.string(),
|
|
198
|
+
field: z2.string(),
|
|
199
|
+
label: z2.string()
|
|
200
|
+
});
|
|
201
|
+
var RelationshipDefinitionSchema = z2.object({
|
|
202
|
+
source: RelationshipLinkSchema,
|
|
203
|
+
target: RelationshipLinkSchema,
|
|
204
|
+
cardinality: RelationshipCardinalitySchema,
|
|
205
|
+
onDelete: OnDeleteBehaviorSchema.default("NONE")
|
|
206
|
+
});
|
|
207
|
+
var ChannelCapabilityTypeSchema = z2.enum([
|
|
208
|
+
"messaging",
|
|
209
|
+
// Text-based: SMS, WhatsApp, Messenger, DMs
|
|
210
|
+
"voice",
|
|
211
|
+
// Audio calls: Phone, WhatsApp Voice, etc.
|
|
212
|
+
"video"
|
|
213
|
+
// Video calls (future)
|
|
214
|
+
]);
|
|
215
|
+
var ChannelCapabilitySchema = z2.object({
|
|
216
|
+
label: z2.string(),
|
|
217
|
+
// Display name: "SMS", "WhatsApp Messages"
|
|
218
|
+
icon: z2.string().optional(),
|
|
219
|
+
// Lucide icon name
|
|
220
|
+
receive: z2.string().optional(),
|
|
221
|
+
// Inbound webhook handler
|
|
222
|
+
send: z2.string().optional()
|
|
223
|
+
// Outbound tool handle
|
|
224
|
+
});
|
|
225
|
+
var ChannelFieldDefinitionSchema = z2.object({
|
|
226
|
+
handle: z2.string(),
|
|
227
|
+
label: z2.string(),
|
|
228
|
+
/** Field definition reference or inline definition */
|
|
229
|
+
definition: z2.union([InlineFieldDefinitionSchema, z2.string()]).optional(),
|
|
230
|
+
/** Marks this field as the identifier field for the channel */
|
|
231
|
+
identifier: z2.boolean().optional(),
|
|
232
|
+
/** Whether this field is required */
|
|
233
|
+
required: z2.boolean().optional(),
|
|
234
|
+
/** Default value when creating a new field */
|
|
235
|
+
defaultValue: z2.object({ value: z2.unknown() }).passthrough().optional(),
|
|
236
|
+
/** Visibility settings for the field */
|
|
237
|
+
visibility: z2.object({
|
|
238
|
+
data: z2.boolean().optional(),
|
|
239
|
+
list: z2.boolean().optional(),
|
|
240
|
+
filters: z2.boolean().optional()
|
|
241
|
+
}).passthrough().optional(),
|
|
242
|
+
/** Permission settings for the field */
|
|
243
|
+
permissions: z2.object({
|
|
244
|
+
read: z2.boolean().optional(),
|
|
245
|
+
write: z2.boolean().optional()
|
|
246
|
+
}).passthrough().optional()
|
|
247
|
+
}).passthrough();
|
|
248
|
+
var ChannelDefinitionSchema = z2.object({
|
|
249
|
+
handle: z2.string(),
|
|
250
|
+
label: z2.string(),
|
|
251
|
+
icon: z2.string().optional(),
|
|
252
|
+
/** Array of field definitions for this channel. One field must have identifier: true. */
|
|
253
|
+
fields: z2.array(ChannelFieldDefinitionSchema),
|
|
254
|
+
// Capabilities keyed by standard type (messaging, voice, video) - all optional
|
|
255
|
+
capabilities: z2.object({
|
|
256
|
+
messaging: ChannelCapabilitySchema.optional(),
|
|
257
|
+
voice: ChannelCapabilitySchema.optional(),
|
|
258
|
+
video: ChannelCapabilitySchema.optional()
|
|
259
|
+
})
|
|
260
|
+
});
|
|
261
|
+
var WorkflowActionInputSchema = z2.object({
|
|
262
|
+
key: z2.string(),
|
|
263
|
+
label: z2.string(),
|
|
264
|
+
fieldRef: z2.object({
|
|
265
|
+
fieldHandle: z2.string(),
|
|
266
|
+
entityHandle: z2.string()
|
|
267
|
+
}).optional(),
|
|
268
|
+
template: z2.string().optional()
|
|
269
|
+
});
|
|
270
|
+
var WorkflowActionSchema = z2.object({
|
|
271
|
+
label: z2.string(),
|
|
272
|
+
handle: z2.string(),
|
|
273
|
+
batch: z2.boolean().optional(),
|
|
274
|
+
entityHandle: z2.string().optional(),
|
|
275
|
+
inputs: z2.array(WorkflowActionInputSchema).optional()
|
|
276
|
+
});
|
|
277
|
+
var WorkflowDefinitionSchema = z2.object({
|
|
278
|
+
path: z2.string(),
|
|
279
|
+
label: z2.string().optional(),
|
|
280
|
+
handle: z2.string().optional(),
|
|
281
|
+
requires: z2.array(ResourceDependencySchema).optional(),
|
|
282
|
+
actions: z2.array(WorkflowActionSchema)
|
|
283
|
+
});
|
|
284
|
+
var PageTypeSchema = z2.enum(["INSTANCE", "LIST"]);
|
|
285
|
+
var PageBlockTypeSchema = z2.enum(["form", "spreadsheet", "kanban", "calendar", "link", "list", "card"]);
|
|
286
|
+
var PageFieldTypeSchema = z2.enum(["STRING", "FILE", "NUMBER", "DATE", "BOOLEAN", "SELECT", "FORM", "RELATIONSHIP"]);
|
|
287
|
+
var PageFieldSourceSchema = z2.object({
|
|
288
|
+
model: z2.string(),
|
|
289
|
+
field: z2.string()
|
|
290
|
+
});
|
|
291
|
+
var PageFormHeaderSchema = z2.object({
|
|
292
|
+
title: z2.string(),
|
|
293
|
+
description: z2.string().optional()
|
|
294
|
+
});
|
|
295
|
+
var PageActionDefinitionSchema = z2.object({
|
|
296
|
+
handle: z2.string(),
|
|
297
|
+
label: z2.string(),
|
|
298
|
+
handler: z2.string(),
|
|
299
|
+
icon: z2.string().optional(),
|
|
300
|
+
variant: z2.enum(["primary", "secondary", "destructive"]).optional(),
|
|
301
|
+
isDisabled: z2.union([z2.boolean(), z2.string()]).optional(),
|
|
302
|
+
isHidden: z2.union([z2.boolean(), z2.string()]).optional()
|
|
303
|
+
});
|
|
304
|
+
var FormV2StylePropsSchema = z2.object({
|
|
305
|
+
id: z2.string(),
|
|
306
|
+
row: z2.number(),
|
|
307
|
+
col: z2.number(),
|
|
308
|
+
className: z2.string().optional(),
|
|
309
|
+
hidden: z2.boolean().optional()
|
|
310
|
+
});
|
|
311
|
+
var FieldSettingButtonPropsSchema = z2.object({
|
|
312
|
+
label: z2.string(),
|
|
313
|
+
variant: z2.enum(["default", "destructive", "outline", "secondary", "ghost", "link"]).optional(),
|
|
314
|
+
size: z2.enum(["default", "sm", "lg", "icon"]).optional(),
|
|
315
|
+
isLoading: z2.boolean().optional(),
|
|
316
|
+
/** Can be boolean or Liquid template string that resolves to boolean */
|
|
317
|
+
isDisabled: z2.union([z2.boolean(), z2.string()]).optional(),
|
|
318
|
+
leftIcon: z2.string().optional()
|
|
319
|
+
});
|
|
320
|
+
var RelationshipExtensionSchema = z2.object({
|
|
321
|
+
model: z2.string()
|
|
322
|
+
});
|
|
323
|
+
var FormLayoutColumnDefinitionSchema = z2.object({
|
|
324
|
+
field: z2.string(),
|
|
325
|
+
colSpan: z2.number(),
|
|
326
|
+
dataType: z2.string().optional(),
|
|
327
|
+
subQuery: z2.unknown().optional()
|
|
328
|
+
});
|
|
329
|
+
var FormLayoutRowDefinitionSchema = z2.object({
|
|
330
|
+
columns: z2.array(FormLayoutColumnDefinitionSchema)
|
|
331
|
+
});
|
|
332
|
+
var FormLayoutConfigDefinitionSchema = z2.object({
|
|
333
|
+
type: z2.literal("form"),
|
|
334
|
+
rows: z2.array(FormLayoutRowDefinitionSchema)
|
|
335
|
+
});
|
|
336
|
+
var InputComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
337
|
+
component: z2.literal("Input"),
|
|
338
|
+
props: z2.object({
|
|
339
|
+
label: z2.string().optional(),
|
|
340
|
+
placeholder: z2.string().optional(),
|
|
341
|
+
helpText: z2.string().optional(),
|
|
342
|
+
type: z2.enum(["text", "number", "email", "password", "tel", "url", "hidden"]).optional(),
|
|
343
|
+
required: z2.boolean().optional(),
|
|
344
|
+
disabled: z2.boolean().optional(),
|
|
345
|
+
value: z2.union([z2.string(), z2.number()]).optional()
|
|
346
|
+
})
|
|
347
|
+
});
|
|
348
|
+
var TextareaComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
349
|
+
component: z2.literal("Textarea"),
|
|
350
|
+
props: z2.object({
|
|
351
|
+
label: z2.string().optional(),
|
|
352
|
+
placeholder: z2.string().optional(),
|
|
353
|
+
helpText: z2.string().optional(),
|
|
354
|
+
required: z2.boolean().optional(),
|
|
355
|
+
disabled: z2.boolean().optional(),
|
|
356
|
+
value: z2.string().optional()
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
var SelectComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
360
|
+
component: z2.literal("Select"),
|
|
361
|
+
props: z2.object({
|
|
362
|
+
label: z2.string().optional(),
|
|
363
|
+
placeholder: z2.string().optional(),
|
|
364
|
+
helpText: z2.string().optional(),
|
|
365
|
+
/** Static items array (will be populated by iterable if using dynamic items) */
|
|
366
|
+
items: z2.union([z2.array(z2.object({ value: z2.string(), label: z2.string() })), z2.string()]).optional(),
|
|
367
|
+
value: z2.string().optional(),
|
|
368
|
+
isDisabled: z2.boolean().optional(),
|
|
369
|
+
required: z2.boolean().optional()
|
|
370
|
+
}),
|
|
371
|
+
relationship: RelationshipExtensionSchema.optional(),
|
|
372
|
+
/** For dynamic items using iterable pattern (e.g., 'system.models') */
|
|
373
|
+
iterable: z2.string().optional(),
|
|
374
|
+
/** Template for each item in the iterable */
|
|
375
|
+
itemTemplate: z2.object({
|
|
376
|
+
value: z2.string(),
|
|
377
|
+
label: z2.string()
|
|
378
|
+
}).optional()
|
|
379
|
+
});
|
|
380
|
+
var ComboboxComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
381
|
+
component: z2.literal("Combobox"),
|
|
382
|
+
props: z2.object({
|
|
383
|
+
label: z2.string().optional(),
|
|
384
|
+
placeholder: z2.string().optional(),
|
|
385
|
+
helpText: z2.string().optional(),
|
|
386
|
+
items: z2.array(z2.object({ value: z2.string(), label: z2.string() })).optional(),
|
|
387
|
+
value: z2.string().optional()
|
|
388
|
+
}),
|
|
389
|
+
relationship: RelationshipExtensionSchema.optional()
|
|
390
|
+
});
|
|
391
|
+
var CheckboxComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
392
|
+
component: z2.literal("Checkbox"),
|
|
393
|
+
props: z2.object({
|
|
394
|
+
label: z2.string().optional(),
|
|
395
|
+
helpText: z2.string().optional(),
|
|
396
|
+
checked: z2.boolean().optional(),
|
|
397
|
+
disabled: z2.boolean().optional()
|
|
398
|
+
})
|
|
399
|
+
});
|
|
400
|
+
var DatePickerComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
401
|
+
component: z2.literal("DatePicker"),
|
|
402
|
+
props: z2.object({
|
|
403
|
+
label: z2.string().optional(),
|
|
404
|
+
helpText: z2.string().optional(),
|
|
405
|
+
value: z2.union([z2.string(), z2.date()]).optional(),
|
|
406
|
+
disabled: z2.boolean().optional()
|
|
407
|
+
})
|
|
408
|
+
});
|
|
409
|
+
var TimePickerComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
410
|
+
component: z2.literal("TimePicker"),
|
|
411
|
+
props: z2.object({
|
|
412
|
+
label: z2.string().optional(),
|
|
413
|
+
helpText: z2.string().optional(),
|
|
414
|
+
value: z2.string().optional(),
|
|
415
|
+
disabled: z2.boolean().optional()
|
|
416
|
+
})
|
|
417
|
+
});
|
|
418
|
+
var ImageSettingComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
419
|
+
component: z2.literal("ImageSetting"),
|
|
420
|
+
props: z2.object({
|
|
421
|
+
label: z2.string().optional(),
|
|
422
|
+
description: z2.string().optional(),
|
|
423
|
+
helpText: z2.string().optional(),
|
|
424
|
+
accept: z2.string().optional()
|
|
425
|
+
})
|
|
426
|
+
});
|
|
427
|
+
var FileSettingComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
428
|
+
component: z2.literal("FileSetting"),
|
|
429
|
+
props: z2.object({
|
|
430
|
+
label: z2.string().optional(),
|
|
431
|
+
description: z2.string().optional(),
|
|
432
|
+
helpText: z2.string().optional(),
|
|
433
|
+
accept: z2.string().optional(),
|
|
434
|
+
required: z2.boolean().optional(),
|
|
435
|
+
button: z2.object({
|
|
436
|
+
label: z2.string().optional(),
|
|
437
|
+
variant: z2.enum(["default", "outline", "ghost", "link"]).optional(),
|
|
438
|
+
size: z2.enum(["sm", "md", "lg"]).optional()
|
|
439
|
+
}).optional()
|
|
440
|
+
})
|
|
441
|
+
});
|
|
442
|
+
var ListItemTemplateSchema = z2.object({
|
|
443
|
+
component: z2.string(),
|
|
444
|
+
span: z2.number().optional(),
|
|
445
|
+
mdSpan: z2.number().optional(),
|
|
446
|
+
lgSpan: z2.number().optional(),
|
|
447
|
+
props: z2.record(z2.string(), z2.unknown())
|
|
448
|
+
});
|
|
449
|
+
var ListComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
450
|
+
component: z2.literal("List"),
|
|
451
|
+
props: z2.object({
|
|
452
|
+
title: z2.string().optional(),
|
|
453
|
+
items: z2.array(z2.object({
|
|
454
|
+
id: z2.string(),
|
|
455
|
+
label: z2.string(),
|
|
456
|
+
description: z2.string().optional()
|
|
457
|
+
})).optional(),
|
|
458
|
+
emptyMessage: z2.string().optional()
|
|
459
|
+
}),
|
|
460
|
+
model: z2.string().optional(),
|
|
461
|
+
labelField: z2.string().optional(),
|
|
462
|
+
descriptionField: z2.string().optional(),
|
|
463
|
+
icon: z2.string().optional(),
|
|
464
|
+
/** Context variable name to iterate over (e.g., 'phone_numbers') */
|
|
465
|
+
iterable: z2.string().optional(),
|
|
466
|
+
/** Template for each item - use {{ item.xyz }} for field values */
|
|
467
|
+
itemTemplate: ListItemTemplateSchema.optional()
|
|
468
|
+
});
|
|
469
|
+
var EmptyFormComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
470
|
+
component: z2.literal("EmptyForm"),
|
|
471
|
+
props: z2.object({
|
|
472
|
+
title: z2.string().optional(),
|
|
473
|
+
description: z2.string().optional(),
|
|
474
|
+
icon: z2.string().optional()
|
|
475
|
+
})
|
|
476
|
+
});
|
|
477
|
+
var AlertComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
478
|
+
component: z2.literal("Alert"),
|
|
479
|
+
props: z2.object({
|
|
480
|
+
title: z2.string(),
|
|
481
|
+
description: z2.string(),
|
|
482
|
+
icon: z2.string().optional(),
|
|
483
|
+
variant: z2.enum(["default", "destructive"]).optional()
|
|
484
|
+
})
|
|
485
|
+
});
|
|
486
|
+
var ModalFormDefinitionSchema = z2.object({
|
|
487
|
+
header: PageFormHeaderSchema,
|
|
488
|
+
handler: z2.string(),
|
|
489
|
+
/** Named dialog template to use instead of inline fields */
|
|
490
|
+
template: z2.string().optional(),
|
|
491
|
+
/** Template-specific params to pass to the dialog */
|
|
492
|
+
templateParams: z2.record(z2.string(), z2.unknown()).optional(),
|
|
493
|
+
/** Inline field definitions (used when template is not specified) */
|
|
494
|
+
fields: z2.lazy(() => z2.array(FormV2ComponentDefinitionSchema)).optional(),
|
|
495
|
+
layout: FormLayoutConfigDefinitionSchema.optional(),
|
|
496
|
+
actions: z2.array(PageActionDefinitionSchema).optional()
|
|
497
|
+
});
|
|
498
|
+
var FieldSettingComponentDefinitionSchema = FormV2StylePropsSchema.extend({
|
|
499
|
+
component: z2.literal("FieldSetting"),
|
|
500
|
+
props: z2.object({
|
|
501
|
+
label: z2.string(),
|
|
502
|
+
description: z2.string().optional(),
|
|
503
|
+
helpText: z2.string().optional(),
|
|
504
|
+
mode: z2.enum(["field", "setting"]).optional(),
|
|
505
|
+
/** Status indicator - can be literal or Liquid template */
|
|
506
|
+
status: z2.string().optional(),
|
|
507
|
+
/** Text to display alongside status badge - can be Liquid template */
|
|
508
|
+
statusText: z2.string().optional(),
|
|
509
|
+
button: FieldSettingButtonPropsSchema
|
|
510
|
+
}),
|
|
511
|
+
modalForm: ModalFormDefinitionSchema.optional()
|
|
512
|
+
});
|
|
513
|
+
var FormV2ComponentDefinitionSchema = z2.discriminatedUnion("component", [
|
|
514
|
+
InputComponentDefinitionSchema,
|
|
515
|
+
TextareaComponentDefinitionSchema,
|
|
516
|
+
SelectComponentDefinitionSchema,
|
|
517
|
+
ComboboxComponentDefinitionSchema,
|
|
518
|
+
CheckboxComponentDefinitionSchema,
|
|
519
|
+
DatePickerComponentDefinitionSchema,
|
|
520
|
+
TimePickerComponentDefinitionSchema,
|
|
521
|
+
FieldSettingComponentDefinitionSchema,
|
|
522
|
+
ImageSettingComponentDefinitionSchema,
|
|
523
|
+
FileSettingComponentDefinitionSchema,
|
|
524
|
+
ListComponentDefinitionSchema,
|
|
525
|
+
EmptyFormComponentDefinitionSchema,
|
|
526
|
+
AlertComponentDefinitionSchema
|
|
527
|
+
]);
|
|
528
|
+
var FormV2PropsDefinitionSchema = z2.object({
|
|
529
|
+
formVersion: z2.literal("v2"),
|
|
530
|
+
id: z2.string().optional(),
|
|
531
|
+
fields: z2.array(FormV2ComponentDefinitionSchema),
|
|
532
|
+
layout: FormLayoutConfigDefinitionSchema,
|
|
533
|
+
/** Optional actions that trigger MCP tool calls */
|
|
534
|
+
actions: z2.array(PageActionDefinitionSchema).optional()
|
|
535
|
+
});
|
|
536
|
+
var CardBlockHeaderSchema = z2.object({
|
|
537
|
+
title: z2.string(),
|
|
538
|
+
description: z2.string().optional(),
|
|
539
|
+
descriptionHref: z2.string().optional()
|
|
540
|
+
});
|
|
541
|
+
var CardBlockDefinitionSchema = z2.object({
|
|
542
|
+
type: z2.literal("card"),
|
|
543
|
+
restructurable: z2.boolean().optional(),
|
|
544
|
+
header: CardBlockHeaderSchema.optional(),
|
|
545
|
+
form: FormV2PropsDefinitionSchema,
|
|
546
|
+
actions: z2.array(PageActionDefinitionSchema).optional(),
|
|
547
|
+
secondaryActions: z2.array(PageActionDefinitionSchema).optional(),
|
|
548
|
+
primaryActions: z2.array(PageActionDefinitionSchema).optional()
|
|
549
|
+
});
|
|
550
|
+
var PageFieldDefinitionBaseSchema = z2.object({
|
|
551
|
+
handle: z2.string(),
|
|
552
|
+
type: PageFieldTypeSchema,
|
|
553
|
+
label: z2.string(),
|
|
554
|
+
description: z2.string().optional(),
|
|
555
|
+
required: z2.boolean().optional(),
|
|
556
|
+
handler: z2.string().optional(),
|
|
557
|
+
source: PageFieldSourceSchema.optional(),
|
|
558
|
+
options: z2.array(z2.object({ value: z2.string(), label: z2.string() })).optional(),
|
|
559
|
+
accept: z2.string().optional(),
|
|
560
|
+
model: z2.string().optional()
|
|
561
|
+
});
|
|
562
|
+
var PageFieldDefinitionSchema = PageFieldDefinitionBaseSchema.extend({
|
|
563
|
+
header: PageFormHeaderSchema.optional(),
|
|
564
|
+
fields: z2.lazy(() => z2.array(PageFieldDefinitionSchema)).optional(),
|
|
565
|
+
actions: z2.lazy(() => z2.array(PageActionDefinitionSchema)).optional()
|
|
566
|
+
});
|
|
567
|
+
var LegacyFormBlockDefinitionSchema = z2.object({
|
|
568
|
+
type: z2.enum(["form", "spreadsheet", "kanban", "calendar", "link"]),
|
|
569
|
+
title: z2.string().optional(),
|
|
570
|
+
fields: z2.array(PageFieldDefinitionSchema).optional(),
|
|
571
|
+
readonly: z2.boolean().optional()
|
|
572
|
+
});
|
|
573
|
+
var ListBlockDefinitionSchema = z2.object({
|
|
574
|
+
type: z2.literal("list"),
|
|
575
|
+
title: z2.string().optional(),
|
|
576
|
+
model: z2.string(),
|
|
577
|
+
labelField: z2.string().optional(),
|
|
578
|
+
descriptionField: z2.string().optional(),
|
|
579
|
+
icon: z2.string().optional(),
|
|
580
|
+
emptyMessage: z2.string().optional()
|
|
581
|
+
});
|
|
582
|
+
var ModelMapperBlockDefinitionSchema = z2.object({
|
|
583
|
+
type: z2.literal("model_mapper"),
|
|
584
|
+
/** The SHARED model handle from install config (e.g., "client", "patient") */
|
|
585
|
+
model: z2.string()
|
|
586
|
+
});
|
|
587
|
+
var PageBlockDefinitionSchema = z2.union([
|
|
588
|
+
CardBlockDefinitionSchema,
|
|
589
|
+
LegacyFormBlockDefinitionSchema,
|
|
590
|
+
ListBlockDefinitionSchema,
|
|
591
|
+
ModelMapperBlockDefinitionSchema
|
|
592
|
+
]);
|
|
593
|
+
var PageContextModeSchema = z2.enum(["first", "many", "count"]);
|
|
594
|
+
var PageContextFiltersSchema = z2.record(
|
|
595
|
+
z2.string(),
|
|
596
|
+
z2.record(
|
|
597
|
+
z2.string(),
|
|
598
|
+
z2.union([PrimitiveSchema, z2.array(PrimitiveSchema), z2.string()])
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
var PageContextItemDefinitionSchema = z2.object({
|
|
602
|
+
/** Model handle to fetch data from */
|
|
603
|
+
model: z2.string(),
|
|
604
|
+
/** Fetch mode: 'first' returns single object, 'many' returns array, 'count' returns number */
|
|
605
|
+
mode: PageContextModeSchema,
|
|
606
|
+
/**
|
|
607
|
+
* Optional filters. Supports:
|
|
608
|
+
* - Simple key-value with Liquid templates: { id: '{{ path_params.id }}' }
|
|
609
|
+
* - StructuredFilter format: { status: { eq: 'APPROVED' } }
|
|
610
|
+
*/
|
|
611
|
+
filters: PageContextFiltersSchema.optional(),
|
|
612
|
+
/** Optional limit for 'many' mode */
|
|
613
|
+
limit: z2.number().optional()
|
|
614
|
+
});
|
|
615
|
+
var PageContextToolItemDefinitionSchema = z2.object({
|
|
616
|
+
/** Tool name to invoke for fetching context data */
|
|
617
|
+
tool: z2.string()
|
|
618
|
+
});
|
|
619
|
+
var PageContextDefinitionSchema = z2.record(
|
|
620
|
+
z2.string(),
|
|
621
|
+
z2.union([PageContextItemDefinitionSchema, PageContextToolItemDefinitionSchema])
|
|
622
|
+
);
|
|
623
|
+
var PageInstanceFilterSchema = z2.object({
|
|
624
|
+
model: z2.string(),
|
|
625
|
+
where: z2.record(z2.string(), z2.unknown()).optional()
|
|
626
|
+
});
|
|
627
|
+
var NavigationItemSchema = z2.object({
|
|
628
|
+
/** Display label (supports Liquid templates) */
|
|
629
|
+
label: z2.string(),
|
|
630
|
+
/** URL href (supports Liquid templates with path_params and context) */
|
|
631
|
+
href: z2.string(),
|
|
632
|
+
/** Optional icon name */
|
|
633
|
+
icon: z2.string().optional()
|
|
634
|
+
});
|
|
635
|
+
var NavigationSectionSchema = z2.object({
|
|
636
|
+
/** Section title (supports Liquid templates) */
|
|
637
|
+
title: z2.string().optional(),
|
|
638
|
+
/** Navigation items in this section */
|
|
639
|
+
items: z2.array(NavigationItemSchema)
|
|
640
|
+
});
|
|
641
|
+
var NavigationSidebarSchema = z2.object({
|
|
642
|
+
/** Sections to display in the sidebar */
|
|
643
|
+
sections: z2.array(NavigationSectionSchema)
|
|
644
|
+
});
|
|
645
|
+
var BreadcrumbItemSchema = z2.object({
|
|
646
|
+
/** Display label (supports Liquid templates) */
|
|
647
|
+
label: z2.string(),
|
|
648
|
+
/** Optional href - if not provided, item is not clickable */
|
|
649
|
+
href: z2.string().optional()
|
|
650
|
+
});
|
|
651
|
+
var NavigationBreadcrumbSchema = z2.object({
|
|
652
|
+
/** Breadcrumb items from left to right */
|
|
653
|
+
items: z2.array(BreadcrumbItemSchema)
|
|
654
|
+
});
|
|
655
|
+
var NavigationConfigSchema = z2.object({
|
|
656
|
+
/** Sidebar navigation */
|
|
657
|
+
sidebar: NavigationSidebarSchema.optional(),
|
|
658
|
+
/** Breadcrumb navigation */
|
|
659
|
+
breadcrumb: NavigationBreadcrumbSchema.optional()
|
|
660
|
+
});
|
|
661
|
+
var PageDefinitionSchema = z2.object({
|
|
662
|
+
type: PageTypeSchema,
|
|
663
|
+
title: z2.string(),
|
|
664
|
+
/** URL path for this page (e.g., '/phone-numbers' or '/phone-numbers/[id]' for dynamic segments) */
|
|
665
|
+
path: z2.string(),
|
|
666
|
+
/** When true, this page is the default landing page for the app installation */
|
|
667
|
+
default: z2.boolean().optional(),
|
|
668
|
+
/**
|
|
669
|
+
* Navigation configuration:
|
|
670
|
+
* - true/false: show/hide in auto-generated navigation
|
|
671
|
+
* - string: Liquid template that evaluates to true/false
|
|
672
|
+
* - NavigationConfigSchema: full navigation override for this page (replaces base navigation)
|
|
673
|
+
*/
|
|
674
|
+
navigation: z2.union([z2.boolean(), z2.string(), NavigationConfigSchema]).optional().default(true),
|
|
675
|
+
blocks: z2.array(PageBlockDefinitionSchema),
|
|
676
|
+
actions: z2.array(PageActionDefinitionSchema).optional(),
|
|
677
|
+
/** Context data to load for Liquid templates. appInstallationId filtering is automatic. */
|
|
678
|
+
context: PageContextDefinitionSchema.optional(),
|
|
679
|
+
/** @deprecated Use context instead */
|
|
680
|
+
filter: PageInstanceFilterSchema.optional()
|
|
681
|
+
});
|
|
682
|
+
var WebhookHttpMethodSchema = z2.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]);
|
|
683
|
+
var WebhookTypeSchema = z2.enum(["WEBHOOK", "CALLBACK"]);
|
|
684
|
+
var WebhookHandlerDefinitionSchema = z2.object({
|
|
685
|
+
description: z2.string().optional(),
|
|
686
|
+
methods: z2.array(WebhookHttpMethodSchema).optional(),
|
|
687
|
+
/** Invocation type: WEBHOOK (fire-and-forget) or CALLBACK (waits for response). Defaults to WEBHOOK. */
|
|
688
|
+
type: WebhookTypeSchema.optional(),
|
|
689
|
+
handler: z2.unknown()
|
|
690
|
+
});
|
|
691
|
+
var WebhooksSchema = z2.record(z2.string(), WebhookHandlerDefinitionSchema);
|
|
692
|
+
var AgentDefinitionSchema = z2.object({
|
|
693
|
+
/** Unique identifier within the app (used for upserts) */
|
|
694
|
+
handle: z2.string().regex(/^[a-z0-9_-]+$/, "Handle must be lowercase alphanumeric with dashes/underscores"),
|
|
695
|
+
/** Display name */
|
|
696
|
+
name: z2.string().min(1),
|
|
697
|
+
/** Description of what the agent does */
|
|
698
|
+
description: z2.string(),
|
|
699
|
+
/** System prompt (static, no templating) */
|
|
700
|
+
system: z2.string(),
|
|
701
|
+
/** Tool names to bind (can be empty for orchestrator agents) */
|
|
702
|
+
tools: z2.array(z2.string()),
|
|
703
|
+
/** Optional LLM model override */
|
|
704
|
+
llmModelId: z2.string().optional(),
|
|
705
|
+
/** Parent agent that can call this agent ('composer' or another agent handle) */
|
|
706
|
+
parentAgent: z2.string().optional()
|
|
707
|
+
});
|
|
708
|
+
var InstallConfigSchema = z2.object({
|
|
709
|
+
env: EnvSchemaSchema.optional(),
|
|
710
|
+
/** SHARED model definitions (mapped to user's existing data during installation) */
|
|
711
|
+
models: z2.array(ModelDefinitionSchema).optional(),
|
|
712
|
+
/** Relationship definitions between SHARED models */
|
|
713
|
+
relationships: z2.array(RelationshipDefinitionSchema).optional()
|
|
714
|
+
});
|
|
715
|
+
var ProvisionConfigSchema = z2.object({
|
|
716
|
+
env: EnvSchemaSchema.optional(),
|
|
717
|
+
/** INTERNAL model definitions (app-owned, not visible to users) */
|
|
718
|
+
models: z2.array(ModelDefinitionSchema).optional(),
|
|
719
|
+
/** Relationship definitions between INTERNAL models */
|
|
720
|
+
relationships: z2.array(RelationshipDefinitionSchema).optional(),
|
|
721
|
+
channels: z2.array(ChannelDefinitionSchema).optional(),
|
|
722
|
+
workflows: z2.array(WorkflowDefinitionSchema).optional(),
|
|
723
|
+
/** Base navigation configuration for all pages (can be overridden per page) */
|
|
724
|
+
navigation: NavigationConfigSchema.optional(),
|
|
725
|
+
pages: z2.array(PageDefinitionSchema).optional()
|
|
726
|
+
});
|
|
727
|
+
var SkedyulConfigSchema = z2.object({
|
|
728
|
+
name: z2.string(),
|
|
729
|
+
version: z2.string().optional(),
|
|
730
|
+
description: z2.string().optional(),
|
|
731
|
+
computeLayer: ComputeLayerTypeSchema.optional(),
|
|
732
|
+
tools: z2.unknown().optional(),
|
|
733
|
+
webhooks: z2.unknown().optional(),
|
|
734
|
+
provision: z2.union([ProvisionConfigSchema, z2.unknown()]).optional(),
|
|
735
|
+
agents: z2.array(AgentDefinitionSchema).optional()
|
|
736
|
+
});
|
|
737
|
+
function safeParseConfig(data) {
|
|
738
|
+
const result = SkedyulConfigSchema.safeParse(data);
|
|
739
|
+
return result.success ? result.data : null;
|
|
740
|
+
}
|
|
741
|
+
var MessageSendChannelSchema = z2.object({
|
|
742
|
+
id: z2.string(),
|
|
743
|
+
handle: z2.string(),
|
|
744
|
+
identifierValue: z2.string()
|
|
745
|
+
});
|
|
746
|
+
var MessageSendSubscriptionSchema = z2.object({
|
|
747
|
+
id: z2.string(),
|
|
748
|
+
identifierValue: z2.string()
|
|
749
|
+
});
|
|
750
|
+
var MessageSendContactSchema = z2.object({
|
|
751
|
+
id: z2.string(),
|
|
752
|
+
name: z2.string().optional()
|
|
753
|
+
});
|
|
754
|
+
var MessageSendMessageSchema = z2.object({
|
|
755
|
+
id: z2.string(),
|
|
756
|
+
content: z2.string(),
|
|
757
|
+
contentRaw: z2.string().optional(),
|
|
758
|
+
title: z2.string().optional()
|
|
759
|
+
});
|
|
760
|
+
var MessageSendInputSchema = z2.object({
|
|
761
|
+
channel: MessageSendChannelSchema,
|
|
762
|
+
subscription: MessageSendSubscriptionSchema,
|
|
763
|
+
contact: MessageSendContactSchema,
|
|
764
|
+
message: MessageSendMessageSchema
|
|
765
|
+
});
|
|
766
|
+
var MessageSendOutputSchema = z2.object({
|
|
767
|
+
status: z2.enum(["sent", "queued", "failed"]),
|
|
768
|
+
remoteId: z2.string().optional()
|
|
769
|
+
});
|
|
770
|
+
function isModelDependency(dep) {
|
|
771
|
+
return "model" in dep;
|
|
772
|
+
}
|
|
773
|
+
function isChannelDependency(dep) {
|
|
774
|
+
return "channel" in dep;
|
|
775
|
+
}
|
|
776
|
+
function isWorkflowDependency(dep) {
|
|
777
|
+
return "workflow" in dep;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/server/index.ts
|
|
781
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
782
|
+
import * as z5 from "zod";
|
|
783
|
+
|
|
784
|
+
// src/core/service.ts
|
|
785
|
+
var CoreApiService = class {
|
|
786
|
+
register(service) {
|
|
787
|
+
this.service = service;
|
|
788
|
+
}
|
|
789
|
+
getService() {
|
|
790
|
+
return this.service;
|
|
791
|
+
}
|
|
792
|
+
setWebhookHandler(handler) {
|
|
793
|
+
this.webhookHandler = handler;
|
|
794
|
+
}
|
|
795
|
+
async dispatchWebhook(request) {
|
|
796
|
+
if (!this.webhookHandler) {
|
|
797
|
+
return { status: 404 };
|
|
798
|
+
}
|
|
799
|
+
return this.webhookHandler(request);
|
|
800
|
+
}
|
|
801
|
+
async callCreateChannel(channel) {
|
|
802
|
+
return this.service?.createCommunicationChannel(channel);
|
|
803
|
+
}
|
|
804
|
+
async callUpdateChannel(channel) {
|
|
805
|
+
return this.service?.updateCommunicationChannel(channel);
|
|
806
|
+
}
|
|
807
|
+
async callDeleteChannel(id) {
|
|
808
|
+
return this.service?.deleteCommunicationChannel(id);
|
|
809
|
+
}
|
|
810
|
+
async callGetChannel(id) {
|
|
811
|
+
return this.service?.getCommunicationChannel(id);
|
|
812
|
+
}
|
|
813
|
+
async callListChannels() {
|
|
814
|
+
return this.service?.getCommunicationChannels();
|
|
815
|
+
}
|
|
816
|
+
async callGetWorkplace(id) {
|
|
817
|
+
return this.service?.getWorkplace(id);
|
|
818
|
+
}
|
|
819
|
+
async callListWorkplaces() {
|
|
820
|
+
return this.service?.listWorkplaces();
|
|
821
|
+
}
|
|
822
|
+
async callSendMessage(args) {
|
|
823
|
+
return this.service?.sendMessage(args);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
var coreApiService = new CoreApiService();
|
|
827
|
+
|
|
828
|
+
// src/server/utils/schema.ts
|
|
829
|
+
import * as z3 from "zod";
|
|
830
|
+
function normalizeBilling(billing) {
|
|
831
|
+
if (!billing || typeof billing.credits !== "number") {
|
|
832
|
+
return { credits: 0 };
|
|
833
|
+
}
|
|
834
|
+
return billing;
|
|
835
|
+
}
|
|
836
|
+
function toJsonSchema(schema) {
|
|
837
|
+
if (!schema) return void 0;
|
|
838
|
+
try {
|
|
839
|
+
return z3.toJSONSchema(schema, {
|
|
840
|
+
unrepresentable: "any"
|
|
841
|
+
// Handle z.date(), z.bigint() etc gracefully
|
|
842
|
+
});
|
|
843
|
+
} catch (err) {
|
|
844
|
+
console.error("[toJsonSchema] Failed to convert schema:", err);
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
function isToolSchemaWithJson(schema) {
|
|
849
|
+
return Boolean(
|
|
850
|
+
schema && typeof schema === "object" && "zod" in schema && schema.zod instanceof z3.ZodType
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
function getZodSchema(schema) {
|
|
854
|
+
if (!schema) return void 0;
|
|
855
|
+
if (schema instanceof z3.ZodType) {
|
|
856
|
+
return schema;
|
|
857
|
+
}
|
|
858
|
+
if (isToolSchemaWithJson(schema)) {
|
|
859
|
+
return schema.zod;
|
|
860
|
+
}
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
function getJsonSchemaFromToolSchema(schema) {
|
|
864
|
+
if (!schema) return void 0;
|
|
865
|
+
if (isToolSchemaWithJson(schema) && schema.jsonSchema) {
|
|
866
|
+
return schema.jsonSchema;
|
|
867
|
+
}
|
|
868
|
+
const zodSchema = getZodSchema(schema);
|
|
869
|
+
return toJsonSchema(zodSchema);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/server/utils/env.ts
|
|
873
|
+
function parseJsonRecord(value) {
|
|
874
|
+
if (!value) {
|
|
875
|
+
return {};
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
return JSON.parse(value);
|
|
879
|
+
} catch {
|
|
880
|
+
return {};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function parseNumberEnv(value) {
|
|
884
|
+
if (!value) {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const parsed = Number.parseInt(value, 10);
|
|
888
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
889
|
+
}
|
|
890
|
+
function mergeRuntimeEnv() {
|
|
891
|
+
const bakedEnv = parseJsonRecord(process.env.MCP_ENV_JSON);
|
|
892
|
+
const runtimeEnv = parseJsonRecord(process.env.MCP_ENV);
|
|
893
|
+
const merged = { ...bakedEnv, ...runtimeEnv };
|
|
894
|
+
Object.assign(process.env, merged);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/server/utils/http.ts
|
|
898
|
+
function readRawRequestBody(req) {
|
|
899
|
+
return new Promise((resolve2, reject) => {
|
|
900
|
+
let body = "";
|
|
901
|
+
req.on("data", (chunk) => {
|
|
902
|
+
body += chunk.toString();
|
|
903
|
+
});
|
|
904
|
+
req.on("end", () => {
|
|
905
|
+
resolve2(body);
|
|
906
|
+
});
|
|
907
|
+
req.on("error", reject);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async function parseJSONBody(req) {
|
|
911
|
+
const rawBody = await readRawRequestBody(req);
|
|
912
|
+
try {
|
|
913
|
+
return rawBody ? JSON.parse(rawBody) : {};
|
|
914
|
+
} catch (err) {
|
|
915
|
+
throw err;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function sendJSON(res, statusCode, data) {
|
|
919
|
+
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
920
|
+
res.end(JSON.stringify(data));
|
|
921
|
+
}
|
|
922
|
+
function getDefaultHeaders(options) {
|
|
923
|
+
return {
|
|
924
|
+
"Content-Type": "application/json",
|
|
925
|
+
"Access-Control-Allow-Origin": options?.allowOrigin ?? "*",
|
|
926
|
+
"Access-Control-Allow-Methods": options?.allowMethods ?? "GET, POST, OPTIONS",
|
|
927
|
+
"Access-Control-Allow-Headers": options?.allowHeaders ?? "Content-Type"
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function createResponse(statusCode, body, headers) {
|
|
931
|
+
return {
|
|
932
|
+
statusCode,
|
|
933
|
+
headers,
|
|
934
|
+
body: JSON.stringify(body)
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function getListeningPort(config) {
|
|
938
|
+
const envPort = Number.parseInt(process.env.PORT ?? "", 10);
|
|
939
|
+
if (!Number.isNaN(envPort)) {
|
|
940
|
+
return envPort;
|
|
941
|
+
}
|
|
942
|
+
return config.defaultPort ?? 3e3;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/core/client.ts
|
|
946
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
947
|
+
import { z as z4 } from "zod/v4";
|
|
948
|
+
var requestConfigStorage = new AsyncLocalStorage();
|
|
949
|
+
var globalConfig = {
|
|
950
|
+
baseUrl: process.env.SKEDYUL_API_URL ?? process.env.SKEDYUL_NODE_URL ?? "",
|
|
951
|
+
apiToken: process.env.SKEDYUL_API_TOKEN ?? ""
|
|
952
|
+
};
|
|
953
|
+
function runWithConfig(config, fn) {
|
|
954
|
+
return requestConfigStorage.run(config, fn);
|
|
955
|
+
}
|
|
956
|
+
function getEffectiveConfig() {
|
|
957
|
+
const requestConfig = requestConfigStorage.getStore();
|
|
958
|
+
if (requestConfig?.baseUrl && requestConfig?.apiToken) {
|
|
959
|
+
return requestConfig;
|
|
960
|
+
}
|
|
961
|
+
return globalConfig;
|
|
962
|
+
}
|
|
963
|
+
function configure(options) {
|
|
964
|
+
globalConfig = {
|
|
965
|
+
...globalConfig,
|
|
966
|
+
...options
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function getConfig() {
|
|
970
|
+
return getEffectiveConfig();
|
|
971
|
+
}
|
|
972
|
+
async function callCore(method, params) {
|
|
973
|
+
const effectiveConfig = getEffectiveConfig();
|
|
974
|
+
const { baseUrl, apiToken } = effectiveConfig;
|
|
975
|
+
if (!baseUrl) {
|
|
976
|
+
throw new Error(
|
|
977
|
+
"Skedyul client not configured: missing baseUrl. Set SKEDYUL_API_URL environment variable or call configure()."
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
if (!apiToken) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
"Skedyul client not configured: missing apiToken. Set SKEDYUL_API_TOKEN environment variable or call configure()."
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const headers = {
|
|
986
|
+
"Content-Type": "application/json",
|
|
987
|
+
Authorization: `Bearer ${apiToken}`
|
|
988
|
+
};
|
|
989
|
+
const fetchUrl = `${baseUrl}/api/core`;
|
|
990
|
+
const response = await fetch(fetchUrl, {
|
|
991
|
+
method: "POST",
|
|
992
|
+
headers,
|
|
993
|
+
body: JSON.stringify({ method, params })
|
|
994
|
+
});
|
|
995
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
996
|
+
if (!contentType.includes("application/json")) {
|
|
997
|
+
const text = await response.text();
|
|
998
|
+
console.error(`[skedyul-node] Core API error: ${response.status} - ${text.slice(0, 200)}`);
|
|
999
|
+
throw new Error(`Core API returned non-JSON response (${response.status}): ${text.slice(0, 100)}`);
|
|
1000
|
+
}
|
|
1001
|
+
const payload = await response.json();
|
|
1002
|
+
if (!payload.success) {
|
|
1003
|
+
const message = payload.errors?.map((e) => e.field ? `${e.field}: ${e.message}` : e.message).join("; ") || "Unknown error";
|
|
1004
|
+
throw new Error(message);
|
|
1005
|
+
}
|
|
1006
|
+
if (!response.ok) {
|
|
1007
|
+
throw new Error(`Core API error (${response.status})`);
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
data: payload.data,
|
|
1011
|
+
errors: payload.errors ?? [],
|
|
1012
|
+
pagination: payload.pagination
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
var workplace = {
|
|
1016
|
+
async list(args) {
|
|
1017
|
+
const { data } = await callCore("workplace.list", {
|
|
1018
|
+
...args?.filter ? { filter: args.filter } : {},
|
|
1019
|
+
...args?.limit ? { limit: args.limit } : {}
|
|
1020
|
+
});
|
|
1021
|
+
return data;
|
|
1022
|
+
},
|
|
1023
|
+
async get(id) {
|
|
1024
|
+
const { data } = await callCore("workplace.get", { id });
|
|
1025
|
+
return data;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
var communicationChannel = {
|
|
1029
|
+
/**
|
|
1030
|
+
* Create a communication channel for an app installation.
|
|
1031
|
+
*
|
|
1032
|
+
* Creates a channel with the given handle from provision.config.ts.
|
|
1033
|
+
* Optionally links a SHARED model to the user's model in a single operation.
|
|
1034
|
+
*
|
|
1035
|
+
* **Requires sk_wkp_ token** - channels are scoped to app installations.
|
|
1036
|
+
*
|
|
1037
|
+
* @param handle - Channel handle from provision.config.ts (e.g., "phone", "email")
|
|
1038
|
+
* @param params - Channel creation parameters
|
|
1039
|
+
*
|
|
1040
|
+
* @example
|
|
1041
|
+
* ```ts
|
|
1042
|
+
* // Create a phone channel and link the contact model
|
|
1043
|
+
* const channel = await communicationChannel.create("phone", {
|
|
1044
|
+
* name: "Sales Line",
|
|
1045
|
+
* identifierValue: "+61400000000",
|
|
1046
|
+
* link: {
|
|
1047
|
+
* handle: "contact", // SHARED model from provision config
|
|
1048
|
+
* targetModelId: modelId, // User's selected model
|
|
1049
|
+
* },
|
|
1050
|
+
* });
|
|
1051
|
+
* ```
|
|
1052
|
+
*/
|
|
1053
|
+
async create(handle, params) {
|
|
1054
|
+
const { data } = await callCore("communicationChannel.create", {
|
|
1055
|
+
handle,
|
|
1056
|
+
...params
|
|
1057
|
+
});
|
|
1058
|
+
return data;
|
|
1059
|
+
},
|
|
1060
|
+
/**
|
|
1061
|
+
* List communication channels with optional filters.
|
|
1062
|
+
*
|
|
1063
|
+
* @example
|
|
1064
|
+
* ```ts
|
|
1065
|
+
* // Find channel by phone number
|
|
1066
|
+
* const channels = await communicationChannel.list({
|
|
1067
|
+
* filter: { identifierValue: '+1234567890' },
|
|
1068
|
+
* limit: 1,
|
|
1069
|
+
* });
|
|
1070
|
+
* ```
|
|
1071
|
+
*/
|
|
1072
|
+
async list(args) {
|
|
1073
|
+
const { data } = await callCore("communicationChannel.list", {
|
|
1074
|
+
...args?.filter ? { filter: args.filter } : {},
|
|
1075
|
+
...args?.limit ? { limit: args.limit } : {}
|
|
1076
|
+
});
|
|
1077
|
+
return data;
|
|
1078
|
+
},
|
|
1079
|
+
async get(id) {
|
|
1080
|
+
const { data } = await callCore("communicationChannel.get", { id });
|
|
1081
|
+
return data;
|
|
1082
|
+
},
|
|
1083
|
+
/**
|
|
1084
|
+
* Receive an inbound message on a communication channel.
|
|
1085
|
+
*
|
|
1086
|
+
* This is typically called from webhook handlers to process incoming messages
|
|
1087
|
+
* (e.g., SMS from Twilio, emails, WhatsApp messages).
|
|
1088
|
+
*
|
|
1089
|
+
* @example
|
|
1090
|
+
* ```ts
|
|
1091
|
+
* // In a webhook handler
|
|
1092
|
+
* const result = await communicationChannel.receiveMessage({
|
|
1093
|
+
* communicationChannelId: channel.id,
|
|
1094
|
+
* from: '+1234567890',
|
|
1095
|
+
* message: 'Hello!',
|
|
1096
|
+
* remoteId: 'twilio-message-sid-123',
|
|
1097
|
+
* });
|
|
1098
|
+
* ```
|
|
1099
|
+
*/
|
|
1100
|
+
async receiveMessage(input) {
|
|
1101
|
+
const { data } = await callCore("communicationChannel.receiveMessage", {
|
|
1102
|
+
communicationChannelId: input.communicationChannelId,
|
|
1103
|
+
from: input.from,
|
|
1104
|
+
message: input.message,
|
|
1105
|
+
contact: input.contact,
|
|
1106
|
+
...input.remoteId ? { remoteId: input.remoteId } : {}
|
|
1107
|
+
});
|
|
1108
|
+
return data;
|
|
1109
|
+
},
|
|
1110
|
+
/**
|
|
1111
|
+
* Update a communication channel's properties.
|
|
1112
|
+
*
|
|
1113
|
+
* @param channelId - The ID of the channel to update
|
|
1114
|
+
* @param params - The properties to update (e.g., name)
|
|
1115
|
+
*
|
|
1116
|
+
* @example
|
|
1117
|
+
* ```ts
|
|
1118
|
+
* const channel = await communicationChannel.update('channel-id-123', {
|
|
1119
|
+
* name: 'New Channel Name',
|
|
1120
|
+
* })
|
|
1121
|
+
* ```
|
|
1122
|
+
*/
|
|
1123
|
+
async update(channelId, params) {
|
|
1124
|
+
const { data } = await callCore("communicationChannel.update", {
|
|
1125
|
+
communicationChannelId: channelId,
|
|
1126
|
+
...params
|
|
1127
|
+
});
|
|
1128
|
+
return data;
|
|
1129
|
+
},
|
|
1130
|
+
/**
|
|
1131
|
+
* Remove a communication channel and its associated resources.
|
|
1132
|
+
*
|
|
1133
|
+
* Deletes the channel and cascades:
|
|
1134
|
+
* - EnvVariables scoped to this channel
|
|
1135
|
+
* - AppFields scoped to this channel
|
|
1136
|
+
* - AppResourceInstances scoped to this channel
|
|
1137
|
+
* - CommunicationChannelSubscriptions (Prisma cascade)
|
|
1138
|
+
*
|
|
1139
|
+
* ChatMessages are preserved with subscriptionId set to null.
|
|
1140
|
+
*
|
|
1141
|
+
* @example
|
|
1142
|
+
* ```ts
|
|
1143
|
+
* const { success } = await communicationChannel.remove('channel-id-123')
|
|
1144
|
+
* ```
|
|
1145
|
+
*/
|
|
1146
|
+
async remove(channelId) {
|
|
1147
|
+
const { data } = await callCore("communicationChannel.remove", {
|
|
1148
|
+
communicationChannelId: channelId
|
|
1149
|
+
});
|
|
1150
|
+
return data;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
var instance = {
|
|
1154
|
+
/**
|
|
1155
|
+
* List instances of an internal model.
|
|
1156
|
+
*
|
|
1157
|
+
* The API token determines the context:
|
|
1158
|
+
* - sk_wkp_ tokens: scoped to the token's app installation
|
|
1159
|
+
* - sk_app_ tokens: searches across ALL installations for the app
|
|
1160
|
+
*
|
|
1161
|
+
* @example
|
|
1162
|
+
* ```ts
|
|
1163
|
+
* // List with filters
|
|
1164
|
+
* const { data, pagination } = await instance.list('compliance_record', {
|
|
1165
|
+
* filter: { status: 'pending' },
|
|
1166
|
+
* page: 1,
|
|
1167
|
+
* limit: 10,
|
|
1168
|
+
* })
|
|
1169
|
+
*
|
|
1170
|
+
* // Cross-installation search (with sk_app_ token)
|
|
1171
|
+
* const { data } = await instance.list('phone_number', {
|
|
1172
|
+
* filter: { phone: '+1234567890' },
|
|
1173
|
+
* })
|
|
1174
|
+
* ```
|
|
1175
|
+
*/
|
|
1176
|
+
async list(modelHandle, args) {
|
|
1177
|
+
const { data, pagination } = await callCore("instance.list", {
|
|
1178
|
+
modelHandle,
|
|
1179
|
+
...args?.page !== void 0 ? { page: args.page } : {},
|
|
1180
|
+
...args?.limit !== void 0 ? { limit: args.limit } : {},
|
|
1181
|
+
...args?.filter ? { filter: args.filter } : {}
|
|
1182
|
+
});
|
|
1183
|
+
return {
|
|
1184
|
+
data,
|
|
1185
|
+
pagination: pagination ?? { page: 1, total: 0, hasMore: false, limit: args?.limit ?? 50 }
|
|
1186
|
+
};
|
|
1187
|
+
},
|
|
1188
|
+
/**
|
|
1189
|
+
* Get a single instance by ID.
|
|
1190
|
+
*
|
|
1191
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1192
|
+
*
|
|
1193
|
+
* @example
|
|
1194
|
+
* ```ts
|
|
1195
|
+
* const record = await instance.get('phone_number', 'ins_abc123')
|
|
1196
|
+
* ```
|
|
1197
|
+
*/
|
|
1198
|
+
async get(modelHandle, id) {
|
|
1199
|
+
const { data } = await callCore("instance.get", {
|
|
1200
|
+
modelHandle,
|
|
1201
|
+
id
|
|
1202
|
+
});
|
|
1203
|
+
return data;
|
|
1204
|
+
},
|
|
1205
|
+
/**
|
|
1206
|
+
* Create a new instance of an internal model.
|
|
1207
|
+
*
|
|
1208
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1209
|
+
*
|
|
1210
|
+
* @example
|
|
1211
|
+
* ```ts
|
|
1212
|
+
* const newRecord = await instance.create('compliance_record', {
|
|
1213
|
+
* status: 'pending',
|
|
1214
|
+
* document_url: 'https://...',
|
|
1215
|
+
* })
|
|
1216
|
+
* ```
|
|
1217
|
+
*/
|
|
1218
|
+
async create(modelHandle, data) {
|
|
1219
|
+
const { data: instance2 } = await callCore("instance.create", {
|
|
1220
|
+
modelHandle,
|
|
1221
|
+
data
|
|
1222
|
+
});
|
|
1223
|
+
return instance2;
|
|
1224
|
+
},
|
|
1225
|
+
/**
|
|
1226
|
+
* Update an existing instance.
|
|
1227
|
+
*
|
|
1228
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1229
|
+
*
|
|
1230
|
+
* @example
|
|
1231
|
+
* ```ts
|
|
1232
|
+
* const updated = await instance.update('compliance_record', 'ins_abc123', {
|
|
1233
|
+
* status: 'approved',
|
|
1234
|
+
* bundle_sid: 'BU123456',
|
|
1235
|
+
* })
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
async update(modelHandle, id, data) {
|
|
1239
|
+
const { data: instance2 } = await callCore("instance.update", {
|
|
1240
|
+
modelHandle,
|
|
1241
|
+
id,
|
|
1242
|
+
data
|
|
1243
|
+
});
|
|
1244
|
+
return instance2;
|
|
1245
|
+
},
|
|
1246
|
+
/**
|
|
1247
|
+
* Delete an existing instance.
|
|
1248
|
+
*
|
|
1249
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1250
|
+
*
|
|
1251
|
+
* @example
|
|
1252
|
+
* ```ts
|
|
1253
|
+
* const { deleted } = await instance.delete('phone_number', 'ins_abc123')
|
|
1254
|
+
* ```
|
|
1255
|
+
*/
|
|
1256
|
+
async delete(modelHandle, id) {
|
|
1257
|
+
const { data } = await callCore("instance.delete", {
|
|
1258
|
+
modelHandle,
|
|
1259
|
+
id
|
|
1260
|
+
});
|
|
1261
|
+
return data;
|
|
1262
|
+
},
|
|
1263
|
+
/**
|
|
1264
|
+
* Delete multiple instances of an internal model in a single batch operation.
|
|
1265
|
+
*
|
|
1266
|
+
* This is more efficient than calling delete() multiple times as it reduces
|
|
1267
|
+
* API overhead and executes all deletes in a single transaction.
|
|
1268
|
+
*
|
|
1269
|
+
* Supports two modes:
|
|
1270
|
+
* - **By IDs**: Delete specific instances by their IDs
|
|
1271
|
+
* - **By Filter**: Delete instances matching a StructuredFilter
|
|
1272
|
+
*
|
|
1273
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1274
|
+
*
|
|
1275
|
+
* @param modelHandle - The model handle from provision config
|
|
1276
|
+
* @param options - Either { ids: string[] } or { filter: StructuredFilter }
|
|
1277
|
+
* @returns Object containing deleted instance IDs and any errors that occurred
|
|
1278
|
+
*
|
|
1279
|
+
* @example
|
|
1280
|
+
* ```ts
|
|
1281
|
+
* // Delete by IDs
|
|
1282
|
+
* const { deleted, errors } = await instance.deleteMany('panel_result', {
|
|
1283
|
+
* ids: ['ins_abc123', 'ins_def456'],
|
|
1284
|
+
* })
|
|
1285
|
+
*
|
|
1286
|
+
* // Delete by filter
|
|
1287
|
+
* const { deleted, errors } = await instance.deleteMany('panel_result', {
|
|
1288
|
+
* filter: { status: { eq: 'pending' } },
|
|
1289
|
+
* })
|
|
1290
|
+
*
|
|
1291
|
+
* if (errors.length > 0) {
|
|
1292
|
+
* console.log('Some items failed:', errors)
|
|
1293
|
+
* }
|
|
1294
|
+
* console.log('Deleted:', deleted.length, 'instances')
|
|
1295
|
+
* ```
|
|
1296
|
+
*/
|
|
1297
|
+
async deleteMany(modelHandle, options) {
|
|
1298
|
+
const { data } = await callCore("instance.deleteMany", {
|
|
1299
|
+
modelHandle,
|
|
1300
|
+
...options
|
|
1301
|
+
});
|
|
1302
|
+
return data;
|
|
1303
|
+
},
|
|
1304
|
+
/**
|
|
1305
|
+
* Create multiple instances of an internal model in a single batch operation.
|
|
1306
|
+
*
|
|
1307
|
+
* This is more efficient than calling create() multiple times as it reduces
|
|
1308
|
+
* API overhead and executes all creates in a single transaction.
|
|
1309
|
+
*
|
|
1310
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1311
|
+
*
|
|
1312
|
+
* @param modelHandle - The model handle from provision config
|
|
1313
|
+
* @param items - Array of data objects to create as instances
|
|
1314
|
+
* @returns Object containing created instances and any errors that occurred
|
|
1315
|
+
*
|
|
1316
|
+
* @example
|
|
1317
|
+
* ```ts
|
|
1318
|
+
* const { created, errors } = await instance.createMany('panel_result', [
|
|
1319
|
+
* { test_name: 'Glucose', value_string: '5.2', unit: 'mmol/L' },
|
|
1320
|
+
* { test_name: 'Creatinine', value_string: '80', unit: 'umol/L' },
|
|
1321
|
+
* ])
|
|
1322
|
+
*
|
|
1323
|
+
* if (errors.length > 0) {
|
|
1324
|
+
* console.log('Some items failed:', errors)
|
|
1325
|
+
* }
|
|
1326
|
+
* console.log('Created:', created.length, 'instances')
|
|
1327
|
+
* ```
|
|
1328
|
+
*/
|
|
1329
|
+
async createMany(modelHandle, items) {
|
|
1330
|
+
const { data } = await callCore("instance.createMany", {
|
|
1331
|
+
modelHandle,
|
|
1332
|
+
items
|
|
1333
|
+
});
|
|
1334
|
+
return data;
|
|
1335
|
+
},
|
|
1336
|
+
/**
|
|
1337
|
+
* Update multiple instances of an internal model in a single batch operation.
|
|
1338
|
+
*
|
|
1339
|
+
* This is more efficient than calling update() multiple times as it reduces
|
|
1340
|
+
* API overhead and executes all updates in a single transaction.
|
|
1341
|
+
*
|
|
1342
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1343
|
+
*
|
|
1344
|
+
* @param modelHandle - The model handle from provision config
|
|
1345
|
+
* @param items - Array of objects containing id and data to update
|
|
1346
|
+
* @returns Object containing updated instances and any errors that occurred
|
|
1347
|
+
*
|
|
1348
|
+
* @example
|
|
1349
|
+
* ```ts
|
|
1350
|
+
* const { updated, errors } = await instance.updateMany('panel_result', [
|
|
1351
|
+
* { id: 'ins_abc123', data: { value_string: '5.5' } },
|
|
1352
|
+
* { id: 'ins_def456', data: { value_string: '85' } },
|
|
1353
|
+
* ])
|
|
1354
|
+
*
|
|
1355
|
+
* if (errors.length > 0) {
|
|
1356
|
+
* console.log('Some items failed:', errors)
|
|
1357
|
+
* }
|
|
1358
|
+
* console.log('Updated:', updated.length, 'instances')
|
|
1359
|
+
* ```
|
|
1360
|
+
*/
|
|
1361
|
+
async updateMany(modelHandle, items) {
|
|
1362
|
+
const { data } = await callCore("instance.updateMany", {
|
|
1363
|
+
modelHandle,
|
|
1364
|
+
items
|
|
1365
|
+
});
|
|
1366
|
+
return data;
|
|
1367
|
+
},
|
|
1368
|
+
/**
|
|
1369
|
+
* Upsert multiple instances of an internal model in a single batch operation.
|
|
1370
|
+
*
|
|
1371
|
+
* Creates instances if they don't exist, updates them if they do (based on matchField).
|
|
1372
|
+
* This is more efficient than calling upsert() multiple times as it reduces
|
|
1373
|
+
* API overhead and executes all upserts in a single transaction.
|
|
1374
|
+
*
|
|
1375
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1376
|
+
*
|
|
1377
|
+
* @param modelHandle - The model handle from provision config
|
|
1378
|
+
* @param items - Array of data objects to upsert as instances
|
|
1379
|
+
* @param matchField - The field handle to match existing instances (e.g., 'vetnostics_id')
|
|
1380
|
+
* @returns Object containing upserted instances with mode and any errors that occurred
|
|
1381
|
+
*
|
|
1382
|
+
* @example
|
|
1383
|
+
* ```ts
|
|
1384
|
+
* const { results, errors } = await instance.upsertMany('panel_result', [
|
|
1385
|
+
* { vetnostics_id: '25-54966975/622/glucose', test_name: 'Glucose', value_string: '5.2' },
|
|
1386
|
+
* { vetnostics_id: '25-54966975/622/creatinine', test_name: 'Creatinine', value_string: '80' },
|
|
1387
|
+
* ], 'vetnostics_id')
|
|
1388
|
+
*
|
|
1389
|
+
* if (errors.length > 0) {
|
|
1390
|
+
* console.log('Some items failed:', errors)
|
|
1391
|
+
* }
|
|
1392
|
+
* const created = results.filter(r => r.mode === 'created')
|
|
1393
|
+
* const updated = results.filter(r => r.mode === 'updated')
|
|
1394
|
+
* console.log('Created:', created.length, 'Updated:', updated.length)
|
|
1395
|
+
* ```
|
|
1396
|
+
*/
|
|
1397
|
+
async upsertMany(modelHandle, items, matchField) {
|
|
1398
|
+
const { data } = await callCore("instance.upsertMany", {
|
|
1399
|
+
modelHandle,
|
|
1400
|
+
items,
|
|
1401
|
+
matchField
|
|
1402
|
+
});
|
|
1403
|
+
return data;
|
|
1404
|
+
},
|
|
1405
|
+
/**
|
|
1406
|
+
* Check if a model is configured (linked) for the current app installation.
|
|
1407
|
+
*
|
|
1408
|
+
* This is useful for best-effort sync scenarios where you want to check
|
|
1409
|
+
* which models are available before attempting to create instances.
|
|
1410
|
+
*
|
|
1411
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1412
|
+
*
|
|
1413
|
+
* @param modelHandle - The model handle from provision config
|
|
1414
|
+
* @returns true if the model is configured and has a valid targetId, false otherwise
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```ts
|
|
1418
|
+
* // Check if models are configured before syncing
|
|
1419
|
+
* const isTestOrderConfigured = await instance.isConfigured('test_order')
|
|
1420
|
+
* const isTestReportConfigured = await instance.isConfigured('test_report')
|
|
1421
|
+
*
|
|
1422
|
+
* if (isTestOrderConfigured) {
|
|
1423
|
+
* await instance.create('test_order', orderData)
|
|
1424
|
+
* }
|
|
1425
|
+
* ```
|
|
1426
|
+
*/
|
|
1427
|
+
async isConfigured(modelHandle) {
|
|
1428
|
+
const { data } = await callCore("instance.isConfigured", {
|
|
1429
|
+
modelHandle
|
|
1430
|
+
});
|
|
1431
|
+
return data.configured;
|
|
1432
|
+
},
|
|
1433
|
+
/**
|
|
1434
|
+
* Check which models from a list are configured for the current app installation.
|
|
1435
|
+
*
|
|
1436
|
+
* This is more efficient than calling isConfigured() multiple times as it
|
|
1437
|
+
* makes a single API call to check all models at once.
|
|
1438
|
+
*
|
|
1439
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1440
|
+
*
|
|
1441
|
+
* @param modelHandles - Array of model handles from provision config
|
|
1442
|
+
* @returns Map of model handle to configuration status
|
|
1443
|
+
*
|
|
1444
|
+
* @example
|
|
1445
|
+
* ```ts
|
|
1446
|
+
* // Check multiple models at once
|
|
1447
|
+
* const configStatus = await instance.getConfiguredModels([
|
|
1448
|
+
* 'test_order',
|
|
1449
|
+
* 'test_report',
|
|
1450
|
+
* 'panel_result',
|
|
1451
|
+
* 'culture_result',
|
|
1452
|
+
* ])
|
|
1453
|
+
*
|
|
1454
|
+
* if (configStatus.get('test_order')) {
|
|
1455
|
+
* // test_order is configured
|
|
1456
|
+
* }
|
|
1457
|
+
* ```
|
|
1458
|
+
*/
|
|
1459
|
+
async getConfiguredModels(modelHandles) {
|
|
1460
|
+
const { data } = await callCore("instance.getConfiguredModels", {
|
|
1461
|
+
modelHandles
|
|
1462
|
+
});
|
|
1463
|
+
return new Map(Object.entries(data));
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
var token = {
|
|
1467
|
+
/**
|
|
1468
|
+
* Exchange an sk_app_ token for an installation-scoped sk_wkp_ JWT.
|
|
1469
|
+
*
|
|
1470
|
+
* **Requires sk_app_ token** - only works with app-level tokens.
|
|
1471
|
+
* Used after identifying the target installation (e.g., via instance.search).
|
|
1472
|
+
*
|
|
1473
|
+
* The returned JWT is short-lived (1 hour) and scoped to the specific installation.
|
|
1474
|
+
*
|
|
1475
|
+
* @example
|
|
1476
|
+
* ```ts
|
|
1477
|
+
* // After finding the installation via instance.search
|
|
1478
|
+
* const { token: scopedToken } = await token.exchange(appInstallationId)
|
|
1479
|
+
*
|
|
1480
|
+
* // Use the scoped token for subsequent operations
|
|
1481
|
+
* runWithConfig({ apiToken: scopedToken, baseUrl: config.baseUrl }, async () => {
|
|
1482
|
+
* const channels = await communicationChannel.list({ filter: { identifierValue: phoneNumber } })
|
|
1483
|
+
* // ...
|
|
1484
|
+
* })
|
|
1485
|
+
* ```
|
|
1486
|
+
*/
|
|
1487
|
+
async exchange(appInstallationId) {
|
|
1488
|
+
const { data } = await callCore("token.exchange", {
|
|
1489
|
+
appInstallationId
|
|
1490
|
+
});
|
|
1491
|
+
return data;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
var file = {
|
|
1495
|
+
/**
|
|
1496
|
+
* Get file metadata by ID.
|
|
1497
|
+
*
|
|
1498
|
+
* Returns file information including name, mimeType, and size.
|
|
1499
|
+
* Files are validated to ensure they belong to the requesting app installation.
|
|
1500
|
+
*
|
|
1501
|
+
* @example
|
|
1502
|
+
* ```ts
|
|
1503
|
+
* // Get file info
|
|
1504
|
+
* const fileInfo = await file.get('fl_abc123')
|
|
1505
|
+
* console.log(fileInfo.name) // 'document.pdf'
|
|
1506
|
+
* console.log(fileInfo.mimeType) // 'application/pdf'
|
|
1507
|
+
* console.log(fileInfo.size) // 12345
|
|
1508
|
+
* ```
|
|
1509
|
+
*/
|
|
1510
|
+
async get(fileId) {
|
|
1511
|
+
const { data } = await callCore("file.get", {
|
|
1512
|
+
fileId
|
|
1513
|
+
});
|
|
1514
|
+
return data;
|
|
1515
|
+
},
|
|
1516
|
+
/**
|
|
1517
|
+
* Get a temporary download URL for an app-scoped file.
|
|
1518
|
+
*
|
|
1519
|
+
* Files are validated to ensure they belong to the requesting app installation.
|
|
1520
|
+
* The returned URL expires in 1 hour.
|
|
1521
|
+
*
|
|
1522
|
+
* @example
|
|
1523
|
+
* ```ts
|
|
1524
|
+
* // Get a download URL for a file
|
|
1525
|
+
* const { url, expiresAt } = await file.getUrl('fl_abc123')
|
|
1526
|
+
*
|
|
1527
|
+
* // Use the URL to download or pass to external services
|
|
1528
|
+
* const response = await fetch(url)
|
|
1529
|
+
* ```
|
|
1530
|
+
*/
|
|
1531
|
+
async getUrl(fileId) {
|
|
1532
|
+
const { data } = await callCore("file.getUrl", {
|
|
1533
|
+
fileId
|
|
1534
|
+
});
|
|
1535
|
+
return data;
|
|
1536
|
+
},
|
|
1537
|
+
/**
|
|
1538
|
+
* Upload file content and create a File record.
|
|
1539
|
+
*
|
|
1540
|
+
* Files are scoped to the app installation and stored privately.
|
|
1541
|
+
* Use file.getUrl() to generate a temporary download URL when needed.
|
|
1542
|
+
*
|
|
1543
|
+
* @example
|
|
1544
|
+
* ```ts
|
|
1545
|
+
* // Upload a file from a Buffer
|
|
1546
|
+
* const buffer = await downloadFromExternalUrl(url)
|
|
1547
|
+
* const { id } = await file.upload({
|
|
1548
|
+
* content: buffer,
|
|
1549
|
+
* name: 'document.pdf',
|
|
1550
|
+
* mimeType: 'application/pdf',
|
|
1551
|
+
* })
|
|
1552
|
+
*
|
|
1553
|
+
* // Upload with a path prefix for organization
|
|
1554
|
+
* const { id } = await file.upload({
|
|
1555
|
+
* content: imageBuffer,
|
|
1556
|
+
* name: 'photo.jpg',
|
|
1557
|
+
* mimeType: 'image/jpeg',
|
|
1558
|
+
* path: 'attachments',
|
|
1559
|
+
* })
|
|
1560
|
+
* ```
|
|
1561
|
+
*/
|
|
1562
|
+
async upload(params) {
|
|
1563
|
+
const content = typeof params.content === "string" ? params.content : params.content.toString("base64");
|
|
1564
|
+
const { data } = await callCore("file.upload", {
|
|
1565
|
+
content,
|
|
1566
|
+
name: params.name,
|
|
1567
|
+
mimeType: params.mimeType,
|
|
1568
|
+
...params.path ? { path: params.path } : {}
|
|
1569
|
+
});
|
|
1570
|
+
return data;
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
var webhook = {
|
|
1574
|
+
/**
|
|
1575
|
+
* Create a webhook registration for a handler.
|
|
1576
|
+
*
|
|
1577
|
+
* Creates a unique URL that external services can call.
|
|
1578
|
+
* When called, the request is routed to the handler defined in webhooks config.
|
|
1579
|
+
*
|
|
1580
|
+
* **Requires sk_wkp_ token** - registrations are scoped to the app installation.
|
|
1581
|
+
*
|
|
1582
|
+
* @param name - Handler name from webhooks config
|
|
1583
|
+
* @param context - Optional metadata passed to the handler when webhook fires
|
|
1584
|
+
* @param options - Optional configuration (e.g., expiration)
|
|
1585
|
+
*
|
|
1586
|
+
* @example
|
|
1587
|
+
* ```ts
|
|
1588
|
+
* // Create a webhook for Twilio compliance callbacks
|
|
1589
|
+
* const { url, id } = await webhook.create('compliance_status', {
|
|
1590
|
+
* bundleSid: bundle.sid,
|
|
1591
|
+
* complianceRecordId: record.id,
|
|
1592
|
+
* })
|
|
1593
|
+
*
|
|
1594
|
+
* // Pass the URL to Twilio
|
|
1595
|
+
* await twilioClient.bundles(bundle.sid).update({
|
|
1596
|
+
* statusCallback: url,
|
|
1597
|
+
* })
|
|
1598
|
+
* ```
|
|
1599
|
+
*/
|
|
1600
|
+
async create(name, context, options) {
|
|
1601
|
+
const { data } = await callCore("webhook.create", {
|
|
1602
|
+
name,
|
|
1603
|
+
...context ? { context } : {},
|
|
1604
|
+
...options?.expiresIn ? { expiresIn: options.expiresIn } : {}
|
|
1605
|
+
});
|
|
1606
|
+
return data;
|
|
1607
|
+
},
|
|
1608
|
+
/**
|
|
1609
|
+
* Delete a webhook registration by ID.
|
|
1610
|
+
*
|
|
1611
|
+
* @param id - Registration ID (whkr_xxx format)
|
|
1612
|
+
* @returns Whether the registration was deleted (false if not found)
|
|
1613
|
+
*
|
|
1614
|
+
* @example
|
|
1615
|
+
* ```ts
|
|
1616
|
+
* const { deleted } = await webhook.delete('whkr_abc123')
|
|
1617
|
+
* ```
|
|
1618
|
+
*/
|
|
1619
|
+
async delete(id) {
|
|
1620
|
+
const { data } = await callCore("webhook.delete", {
|
|
1621
|
+
id
|
|
1622
|
+
});
|
|
1623
|
+
return data;
|
|
1624
|
+
},
|
|
1625
|
+
/**
|
|
1626
|
+
* Delete webhook registrations by handler name.
|
|
1627
|
+
*
|
|
1628
|
+
* Useful for cleaning up all webhooks of a certain type,
|
|
1629
|
+
* or filtering by context values.
|
|
1630
|
+
*
|
|
1631
|
+
* @param name - Handler name from webhooks config
|
|
1632
|
+
* @param options - Optional filter by context values
|
|
1633
|
+
* @returns Number of registrations deleted
|
|
1634
|
+
*
|
|
1635
|
+
* @example
|
|
1636
|
+
* ```ts
|
|
1637
|
+
* // Delete all receive_sms webhooks for this installation
|
|
1638
|
+
* const { count } = await webhook.deleteByName('receive_sms')
|
|
1639
|
+
*
|
|
1640
|
+
* // Delete only webhooks for a specific channel
|
|
1641
|
+
* const { count } = await webhook.deleteByName('receive_sms', {
|
|
1642
|
+
* filter: { communicationChannelId: channel.id },
|
|
1643
|
+
* })
|
|
1644
|
+
* ```
|
|
1645
|
+
*/
|
|
1646
|
+
async deleteByName(name, options) {
|
|
1647
|
+
const { data } = await callCore("webhook.deleteByName", {
|
|
1648
|
+
name,
|
|
1649
|
+
...options?.filter ? { filter: options.filter } : {}
|
|
1650
|
+
});
|
|
1651
|
+
return data;
|
|
1652
|
+
},
|
|
1653
|
+
/**
|
|
1654
|
+
* List webhook registrations for this installation.
|
|
1655
|
+
*
|
|
1656
|
+
* @param options - Optional filter by handler name
|
|
1657
|
+
* @returns Array of webhook registrations
|
|
1658
|
+
*
|
|
1659
|
+
* @example
|
|
1660
|
+
* ```ts
|
|
1661
|
+
* // List all webhooks
|
|
1662
|
+
* const { webhooks } = await webhook.list()
|
|
1663
|
+
*
|
|
1664
|
+
* // List only receive_sms webhooks
|
|
1665
|
+
* const { webhooks } = await webhook.list({ name: 'receive_sms' })
|
|
1666
|
+
* ```
|
|
1667
|
+
*/
|
|
1668
|
+
async list(options) {
|
|
1669
|
+
const { data } = await callCore("webhook.list", {
|
|
1670
|
+
...options?.name ? { name: options.name } : {}
|
|
1671
|
+
});
|
|
1672
|
+
return { webhooks: data };
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
var resource = {
|
|
1676
|
+
/**
|
|
1677
|
+
* Link a SHARED app resource (model) to a user's resource.
|
|
1678
|
+
*
|
|
1679
|
+
* Creates an AppResourceInstance hierarchy for MODEL types.
|
|
1680
|
+
* This establishes the connection between an app's SHARED model
|
|
1681
|
+
* (e.g., 'contact') and a user's actual workplace model (e.g., 'Clients').
|
|
1682
|
+
*
|
|
1683
|
+
* The API token determines the context (app installation is embedded in sk_wkp_ tokens).
|
|
1684
|
+
*
|
|
1685
|
+
* @param params - Link parameters
|
|
1686
|
+
*
|
|
1687
|
+
* @example
|
|
1688
|
+
* ```ts
|
|
1689
|
+
* // Link the SHARED 'contact' model to user's 'Clients' model
|
|
1690
|
+
* const { instanceId } = await resource.link({
|
|
1691
|
+
* handle: 'contact', // SHARED model handle from provision config
|
|
1692
|
+
* targetModelId: modelId, // User's selected model ID
|
|
1693
|
+
* channelId: channel.id, // Optional: scope to communication channel
|
|
1694
|
+
* })
|
|
1695
|
+
* ```
|
|
1696
|
+
*/
|
|
1697
|
+
async link(params) {
|
|
1698
|
+
const { data } = await callCore("resource.link", {
|
|
1699
|
+
...params
|
|
1700
|
+
});
|
|
1701
|
+
return data;
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
function zodSchemaToJsonSchema(schema) {
|
|
1705
|
+
return z4.toJSONSchema(schema);
|
|
1706
|
+
}
|
|
1707
|
+
var ai = {
|
|
1708
|
+
/**
|
|
1709
|
+
* Generate a structured object using AI.
|
|
1710
|
+
*
|
|
1711
|
+
* The AI will generate an object that conforms to the provided Zod schema.
|
|
1712
|
+
* Supports both simple text prompts and multimodal messages with files/images.
|
|
1713
|
+
*
|
|
1714
|
+
* @example
|
|
1715
|
+
* ```ts
|
|
1716
|
+
* // Simple text prompt
|
|
1717
|
+
* const result = await ai.generateObject({
|
|
1718
|
+
* system: 'Extract patient information from the text.',
|
|
1719
|
+
* prompt: 'Patient: Max, Species: Canine, DOB: 2020-01-15',
|
|
1720
|
+
* schema: z.object({
|
|
1721
|
+
* patientName: z.string(),
|
|
1722
|
+
* species: z.string(),
|
|
1723
|
+
* dateOfBirth: z.string().nullable(),
|
|
1724
|
+
* }),
|
|
1725
|
+
* })
|
|
1726
|
+
*
|
|
1727
|
+
* // With files array (simple multimodal)
|
|
1728
|
+
* const result = await ai.generateObject({
|
|
1729
|
+
* model: 'openai/gpt-4o',
|
|
1730
|
+
* system: 'Parse the lab report and extract test results.',
|
|
1731
|
+
* prompt: 'Extract all test results from this report.',
|
|
1732
|
+
* files: ['fl_abc123'],
|
|
1733
|
+
* schema: TestResultsSchema,
|
|
1734
|
+
* })
|
|
1735
|
+
*
|
|
1736
|
+
* // With messages (advanced multimodal)
|
|
1737
|
+
* const result = await ai.generateObject({
|
|
1738
|
+
* model: 'openai/gpt-4o',
|
|
1739
|
+
* system: 'Parse the lab report and extract test results.',
|
|
1740
|
+
* schema: TestResultsSchema,
|
|
1741
|
+
* messages: [
|
|
1742
|
+
* {
|
|
1743
|
+
* role: 'user',
|
|
1744
|
+
* content: [
|
|
1745
|
+
* { type: 'text', text: 'Extract all test results from this report.' },
|
|
1746
|
+
* { type: 'file', fileId: 'fl_abc123' },
|
|
1747
|
+
* ],
|
|
1748
|
+
* },
|
|
1749
|
+
* ],
|
|
1750
|
+
* })
|
|
1751
|
+
* ```
|
|
1752
|
+
*/
|
|
1753
|
+
async generateObject(options) {
|
|
1754
|
+
if (!options.prompt && !options.messages) {
|
|
1755
|
+
throw new Error("Either prompt or messages must be provided");
|
|
1756
|
+
}
|
|
1757
|
+
const jsonSchema = zodSchemaToJsonSchema(options.schema);
|
|
1758
|
+
const { data } = await callCore("ai.generateObject", {
|
|
1759
|
+
...options.model ? { model: options.model } : {},
|
|
1760
|
+
system: options.system,
|
|
1761
|
+
...options.prompt ? { prompt: options.prompt } : {},
|
|
1762
|
+
schema: jsonSchema,
|
|
1763
|
+
...options.files && options.files.length > 0 ? { files: options.files } : {},
|
|
1764
|
+
...options.messages ? { messages: options.messages } : {},
|
|
1765
|
+
...options.maxTokens !== void 0 ? { maxTokens: options.maxTokens } : {},
|
|
1766
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {}
|
|
1767
|
+
});
|
|
1768
|
+
return data;
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
var report = {
|
|
1772
|
+
/**
|
|
1773
|
+
* Generate a report from a template.
|
|
1774
|
+
*
|
|
1775
|
+
* By default, returns a URL to view the report. Set `mode: 'html'` to get
|
|
1776
|
+
* the rendered HTML content directly.
|
|
1777
|
+
*
|
|
1778
|
+
* **Requires sk_wkp_ token** - reports are scoped to workplaces.
|
|
1779
|
+
*
|
|
1780
|
+
* @example
|
|
1781
|
+
* ```ts
|
|
1782
|
+
* // Get report URL (default)
|
|
1783
|
+
* const { url } = await report.generate({
|
|
1784
|
+
* templateHandle: 'lab-results',
|
|
1785
|
+
* arguments: { patient_id: 'ins_abc123' },
|
|
1786
|
+
* })
|
|
1787
|
+
* console.log(url) // https://app.skedyul.com/crux/reports/lab-results/generate?patient_id=ins_abc123
|
|
1788
|
+
*
|
|
1789
|
+
* // Get HTML content
|
|
1790
|
+
* const { html } = await report.generate({
|
|
1791
|
+
* templateHandle: 'lab-results',
|
|
1792
|
+
* arguments: { patient_id: 'ins_abc123' },
|
|
1793
|
+
* mode: 'html',
|
|
1794
|
+
* })
|
|
1795
|
+
* // Use html for email, PDF generation, etc.
|
|
1796
|
+
* ```
|
|
1797
|
+
*/
|
|
1798
|
+
async generate(params) {
|
|
1799
|
+
const { data } = await callCore("report.generate", {
|
|
1800
|
+
templateHandle: params.templateHandle,
|
|
1801
|
+
...params.arguments ? { arguments: params.arguments } : {},
|
|
1802
|
+
...params.mode ? { mode: params.mode } : {}
|
|
1803
|
+
});
|
|
1804
|
+
return data;
|
|
1805
|
+
},
|
|
1806
|
+
/**
|
|
1807
|
+
* Define (create or update) a report from a YAML template.
|
|
1808
|
+
*
|
|
1809
|
+
* If a report with the same handle already exists, it will be updated
|
|
1810
|
+
* and the version number incremented.
|
|
1811
|
+
*
|
|
1812
|
+
* **Requires sk_wkp_ token** - reports are scoped to workplaces.
|
|
1813
|
+
*
|
|
1814
|
+
* @example
|
|
1815
|
+
* ```ts
|
|
1816
|
+
* const { definitionId, handle, version } = await report.define({
|
|
1817
|
+
* yaml: `
|
|
1818
|
+
* handle: patient-summary
|
|
1819
|
+
* name: Patient Summary
|
|
1820
|
+
* sections:
|
|
1821
|
+
* - type: header
|
|
1822
|
+
* title: "{{ patient.name }}"
|
|
1823
|
+
* `,
|
|
1824
|
+
* })
|
|
1825
|
+
* console.log(`Created report ${handle} v${version}`)
|
|
1826
|
+
* ```
|
|
1827
|
+
*/
|
|
1828
|
+
async define(params) {
|
|
1829
|
+
const { data } = await callCore("report.define", {
|
|
1830
|
+
yaml: params.yaml
|
|
1831
|
+
});
|
|
1832
|
+
return data;
|
|
1833
|
+
},
|
|
1834
|
+
/**
|
|
1835
|
+
* List report definitions in the workplace.
|
|
1836
|
+
*
|
|
1837
|
+
* **Requires sk_wkp_ token** - reports are scoped to workplaces.
|
|
1838
|
+
*
|
|
1839
|
+
* @example
|
|
1840
|
+
* ```ts
|
|
1841
|
+
* const { data, pagination } = await report.list({ page: 1, limit: 10 })
|
|
1842
|
+
* for (const def of data) {
|
|
1843
|
+
* console.log(`${def.handle}: ${def.name} (v${def.version})`)
|
|
1844
|
+
* }
|
|
1845
|
+
* ```
|
|
1846
|
+
*/
|
|
1847
|
+
async list(params) {
|
|
1848
|
+
const { data, pagination } = await callCore("report.list", {
|
|
1849
|
+
...params?.page !== void 0 ? { page: params.page } : {},
|
|
1850
|
+
...params?.limit !== void 0 ? { limit: params.limit } : {}
|
|
1851
|
+
});
|
|
1852
|
+
return {
|
|
1853
|
+
data,
|
|
1854
|
+
pagination: pagination ?? { page: 1, total: 0, hasMore: false, limit: params?.limit ?? 50 }
|
|
1855
|
+
};
|
|
1856
|
+
},
|
|
1857
|
+
/**
|
|
1858
|
+
* Get a report definition by handle.
|
|
1859
|
+
*
|
|
1860
|
+
* **Requires sk_wkp_ token** - reports are scoped to workplaces.
|
|
1861
|
+
*
|
|
1862
|
+
* @example
|
|
1863
|
+
* ```ts
|
|
1864
|
+
* const definition = await report.get('lab-results')
|
|
1865
|
+
* if (definition) {
|
|
1866
|
+
* console.log(definition.templateYaml)
|
|
1867
|
+
* }
|
|
1868
|
+
* ```
|
|
1869
|
+
*/
|
|
1870
|
+
async get(handle) {
|
|
1871
|
+
const { data } = await callCore("report.get", {
|
|
1872
|
+
handle
|
|
1873
|
+
});
|
|
1874
|
+
return data;
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
|
|
1878
|
+
// src/errors.ts
|
|
1879
|
+
var InstallError = class extends Error {
|
|
1880
|
+
// Optional: which field caused the error
|
|
1881
|
+
constructor(message, code, field) {
|
|
1882
|
+
super(message);
|
|
1883
|
+
this.name = "InstallError";
|
|
1884
|
+
this.code = code;
|
|
1885
|
+
this.field = field;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
var MissingRequiredFieldError = class extends InstallError {
|
|
1889
|
+
constructor(fieldName, message) {
|
|
1890
|
+
super(
|
|
1891
|
+
message ?? `${fieldName} is required`,
|
|
1892
|
+
"MISSING_REQUIRED_FIELD",
|
|
1893
|
+
fieldName
|
|
1894
|
+
);
|
|
1895
|
+
this.name = "MissingRequiredFieldError";
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
var AuthenticationError = class extends InstallError {
|
|
1899
|
+
constructor(message) {
|
|
1900
|
+
super(message ?? "Authentication failed", "AUTHENTICATION_FAILED");
|
|
1901
|
+
this.name = "AuthenticationError";
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1904
|
+
var InvalidConfigurationError = class extends InstallError {
|
|
1905
|
+
constructor(fieldName, message) {
|
|
1906
|
+
super(
|
|
1907
|
+
message ?? "Invalid configuration",
|
|
1908
|
+
"INVALID_CONFIGURATION",
|
|
1909
|
+
fieldName
|
|
1910
|
+
);
|
|
1911
|
+
this.name = "InvalidConfigurationError";
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
var ConnectionError = class extends InstallError {
|
|
1915
|
+
constructor(message) {
|
|
1916
|
+
super(
|
|
1917
|
+
message ?? "Connection failed",
|
|
1918
|
+
"CONNECTION_FAILED"
|
|
1919
|
+
);
|
|
1920
|
+
this.name = "ConnectionError";
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
var AppAuthInvalidError = class extends Error {
|
|
1924
|
+
constructor(message) {
|
|
1925
|
+
super(message);
|
|
1926
|
+
this.code = "APP_AUTH_INVALID";
|
|
1927
|
+
this.name = "AppAuthInvalidError";
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
// src/server/context-logger.ts
|
|
1932
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
1933
|
+
var logContextStorage = new AsyncLocalStorage2();
|
|
1934
|
+
function runWithLogContext(context, fn) {
|
|
1935
|
+
return logContextStorage.run(context, fn);
|
|
1936
|
+
}
|
|
1937
|
+
function getLogContext() {
|
|
1938
|
+
return logContextStorage.getStore();
|
|
1939
|
+
}
|
|
1940
|
+
function safeStringify(value) {
|
|
1941
|
+
if (value === void 0) return "undefined";
|
|
1942
|
+
if (value === null) return "null";
|
|
1943
|
+
if (typeof value === "string") return value;
|
|
1944
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1945
|
+
if (value instanceof Error) {
|
|
1946
|
+
return `${value.name}: ${value.message}${value.stack ? `
|
|
1947
|
+
${value.stack}` : ""}`;
|
|
1948
|
+
}
|
|
1949
|
+
try {
|
|
1950
|
+
return JSON.stringify(value);
|
|
1951
|
+
} catch {
|
|
1952
|
+
return String(value);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function formatLogWithContext(args) {
|
|
1956
|
+
const context = getLogContext();
|
|
1957
|
+
if (!context?.invocation) {
|
|
1958
|
+
return args;
|
|
1959
|
+
}
|
|
1960
|
+
const contextPrefix = {
|
|
1961
|
+
invocationType: context.invocation.invocationType,
|
|
1962
|
+
...context.invocation.toolHandle && { toolHandle: context.invocation.toolHandle },
|
|
1963
|
+
...context.invocation.serverHookHandle && { serverHookHandle: context.invocation.serverHookHandle },
|
|
1964
|
+
...context.invocation.appInstallationId && { appInstallationId: context.invocation.appInstallationId },
|
|
1965
|
+
...context.invocation.toolCallId && { toolCallId: context.invocation.toolCallId },
|
|
1966
|
+
...context.invocation.workflowId && { workflowId: context.invocation.workflowId },
|
|
1967
|
+
...context.invocation.workflowRunId && { workflowRunId: context.invocation.workflowRunId }
|
|
1968
|
+
};
|
|
1969
|
+
const prefix = `[${JSON.stringify(contextPrefix)}]`;
|
|
1970
|
+
const messageParts = args.map((arg) => {
|
|
1971
|
+
if (typeof arg === "string") return arg;
|
|
1972
|
+
return safeStringify(arg);
|
|
1973
|
+
});
|
|
1974
|
+
return [`${prefix} ${messageParts.join(" ")}`];
|
|
1975
|
+
}
|
|
1976
|
+
var originalConsole = {
|
|
1977
|
+
log: console.log.bind(console),
|
|
1978
|
+
info: console.info.bind(console),
|
|
1979
|
+
warn: console.warn.bind(console),
|
|
1980
|
+
error: console.error.bind(console),
|
|
1981
|
+
debug: console.debug.bind(console)
|
|
1982
|
+
};
|
|
1983
|
+
function installContextLogger() {
|
|
1984
|
+
console.log = (...args) => {
|
|
1985
|
+
originalConsole.log(...formatLogWithContext(args));
|
|
1986
|
+
};
|
|
1987
|
+
console.info = (...args) => {
|
|
1988
|
+
originalConsole.info(...formatLogWithContext(args));
|
|
1989
|
+
};
|
|
1990
|
+
console.warn = (...args) => {
|
|
1991
|
+
originalConsole.warn(...formatLogWithContext(args));
|
|
1992
|
+
};
|
|
1993
|
+
console.error = (...args) => {
|
|
1994
|
+
originalConsole.error(...formatLogWithContext(args));
|
|
1995
|
+
};
|
|
1996
|
+
console.debug = (...args) => {
|
|
1997
|
+
originalConsole.debug(...formatLogWithContext(args));
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/server/logger.ts
|
|
2002
|
+
function createContextLogger() {
|
|
2003
|
+
const log = ((msg, ...args) => {
|
|
2004
|
+
console.log(msg, ...args);
|
|
2005
|
+
});
|
|
2006
|
+
log.info = (msg, ...args) => {
|
|
2007
|
+
console.info(msg, ...args);
|
|
2008
|
+
};
|
|
2009
|
+
log.warn = (msg, ...args) => {
|
|
2010
|
+
console.warn(msg, ...args);
|
|
2011
|
+
};
|
|
2012
|
+
log.error = (msg, ...args) => {
|
|
2013
|
+
console.error(msg, ...args);
|
|
2014
|
+
};
|
|
2015
|
+
log.debug = (msg, ...args) => {
|
|
2016
|
+
console.debug(msg, ...args);
|
|
2017
|
+
};
|
|
2018
|
+
return log;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/server/tool-handler.ts
|
|
2022
|
+
function buildToolMetadata(registry) {
|
|
2023
|
+
return Object.values(registry).map((tool) => {
|
|
2024
|
+
const timeout = typeof tool.timeout === "number" && tool.timeout > 0 ? tool.timeout : 1e4;
|
|
2025
|
+
const retries = typeof tool.retries === "number" && tool.retries >= 1 ? tool.retries : 1;
|
|
2026
|
+
return {
|
|
2027
|
+
name: tool.name,
|
|
2028
|
+
displayName: tool.label || tool.name,
|
|
2029
|
+
description: tool.description,
|
|
2030
|
+
inputSchema: getJsonSchemaFromToolSchema(tool.inputSchema),
|
|
2031
|
+
outputSchema: getJsonSchemaFromToolSchema(tool.outputSchema),
|
|
2032
|
+
timeout,
|
|
2033
|
+
retries
|
|
2034
|
+
};
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
function createRequestState(maxRequests, ttlExtendSeconds, runtimeLabel, toolNames) {
|
|
2038
|
+
let requestCount = 0;
|
|
2039
|
+
let lastRequestTime = Date.now();
|
|
2040
|
+
return {
|
|
2041
|
+
incrementRequestCount() {
|
|
2042
|
+
requestCount += 1;
|
|
2043
|
+
lastRequestTime = Date.now();
|
|
2044
|
+
},
|
|
2045
|
+
shouldShutdown() {
|
|
2046
|
+
return maxRequests !== null && requestCount >= maxRequests;
|
|
2047
|
+
},
|
|
2048
|
+
getHealthStatus() {
|
|
2049
|
+
return {
|
|
2050
|
+
status: "running",
|
|
2051
|
+
requests: requestCount,
|
|
2052
|
+
maxRequests,
|
|
2053
|
+
requestsRemaining: maxRequests !== null ? Math.max(0, maxRequests - requestCount) : null,
|
|
2054
|
+
lastRequestTime,
|
|
2055
|
+
ttlExtendSeconds,
|
|
2056
|
+
runtime: runtimeLabel,
|
|
2057
|
+
tools: [...toolNames]
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
function createCallToolHandler(registry, state, onMaxRequests) {
|
|
2063
|
+
return async function callTool(nameRaw, argsRaw) {
|
|
2064
|
+
const toolName = String(nameRaw);
|
|
2065
|
+
const tool = registry[toolName];
|
|
2066
|
+
if (!tool) {
|
|
2067
|
+
throw new Error(`Tool "${toolName}" not found in registry`);
|
|
2068
|
+
}
|
|
2069
|
+
if (!tool.handler || typeof tool.handler !== "function") {
|
|
2070
|
+
throw new Error(`Tool "${toolName}" handler is not a function`);
|
|
2071
|
+
}
|
|
2072
|
+
const fn = tool.handler;
|
|
2073
|
+
const args = argsRaw ?? {};
|
|
2074
|
+
const estimateMode = args.estimate === true;
|
|
2075
|
+
if (!estimateMode) {
|
|
2076
|
+
state.incrementRequestCount();
|
|
2077
|
+
if (state.shouldShutdown()) {
|
|
2078
|
+
onMaxRequests?.();
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
const requestEnv = args.env ?? {};
|
|
2082
|
+
const originalEnv = { ...process.env };
|
|
2083
|
+
Object.assign(process.env, requestEnv);
|
|
2084
|
+
const invocation = args.invocation;
|
|
2085
|
+
try {
|
|
2086
|
+
const inputs = args.inputs ?? {};
|
|
2087
|
+
const rawContext = args.context ?? {};
|
|
2088
|
+
const app = rawContext.app;
|
|
2089
|
+
const trigger = rawContext.trigger || "agent";
|
|
2090
|
+
let executionContext;
|
|
2091
|
+
const log = createContextLogger();
|
|
2092
|
+
if (trigger === "provision") {
|
|
2093
|
+
executionContext = {
|
|
2094
|
+
trigger: "provision",
|
|
2095
|
+
app,
|
|
2096
|
+
env: process.env,
|
|
2097
|
+
mode: estimateMode ? "estimate" : "execute",
|
|
2098
|
+
invocation,
|
|
2099
|
+
log
|
|
2100
|
+
};
|
|
2101
|
+
} else {
|
|
2102
|
+
const workplace2 = rawContext.workplace;
|
|
2103
|
+
const request = rawContext.request;
|
|
2104
|
+
const appInstallationId = rawContext.appInstallationId;
|
|
2105
|
+
const envVars = process.env;
|
|
2106
|
+
const modeValue = estimateMode ? "estimate" : "execute";
|
|
2107
|
+
if (trigger === "field_change") {
|
|
2108
|
+
const field = rawContext.field;
|
|
2109
|
+
executionContext = { trigger: "field_change", app, appInstallationId, workplace: workplace2, request, env: envVars, mode: modeValue, field, invocation, log };
|
|
2110
|
+
} else if (trigger === "page_action") {
|
|
2111
|
+
const page = rawContext.page;
|
|
2112
|
+
executionContext = { trigger: "page_action", app, appInstallationId, workplace: workplace2, request, env: envVars, mode: modeValue, page, invocation, log };
|
|
2113
|
+
} else if (trigger === "form_submit") {
|
|
2114
|
+
const form = rawContext.form;
|
|
2115
|
+
executionContext = { trigger: "form_submit", app, appInstallationId, workplace: workplace2, request, env: envVars, mode: modeValue, form, invocation, log };
|
|
2116
|
+
} else if (trigger === "workflow") {
|
|
2117
|
+
executionContext = { trigger: "workflow", app, appInstallationId, workplace: workplace2, request, env: envVars, mode: modeValue, invocation, log };
|
|
2118
|
+
} else if (trigger === "page_context") {
|
|
2119
|
+
executionContext = { trigger: "agent", app, appInstallationId, workplace: workplace2, request, env: envVars, mode: modeValue, invocation, log };
|
|
2120
|
+
} else {
|
|
2121
|
+
executionContext = { trigger: "agent", app, appInstallationId, workplace: workplace2, request, env: envVars, mode: modeValue, invocation, log };
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
const requestConfig = {
|
|
2125
|
+
baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
2126
|
+
apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
2127
|
+
};
|
|
2128
|
+
const functionResult = await runWithLogContext({ invocation }, async () => {
|
|
2129
|
+
return await runWithConfig(requestConfig, async () => {
|
|
2130
|
+
return await fn(inputs, executionContext);
|
|
2131
|
+
});
|
|
2132
|
+
});
|
|
2133
|
+
const billing = normalizeBilling(functionResult.billing);
|
|
2134
|
+
return {
|
|
2135
|
+
output: functionResult.output,
|
|
2136
|
+
billing,
|
|
2137
|
+
meta: functionResult.meta ?? {
|
|
2138
|
+
success: true,
|
|
2139
|
+
message: "OK",
|
|
2140
|
+
toolName
|
|
2141
|
+
},
|
|
2142
|
+
effect: functionResult.effect
|
|
2143
|
+
};
|
|
2144
|
+
} catch (error) {
|
|
2145
|
+
if (error instanceof AppAuthInvalidError) {
|
|
2146
|
+
return {
|
|
2147
|
+
output: null,
|
|
2148
|
+
billing: { credits: 0 },
|
|
2149
|
+
meta: {
|
|
2150
|
+
success: false,
|
|
2151
|
+
message: error.message,
|
|
2152
|
+
toolName
|
|
2153
|
+
},
|
|
2154
|
+
error: {
|
|
2155
|
+
code: error.code,
|
|
2156
|
+
message: error.message
|
|
2157
|
+
}
|
|
2158
|
+
// Note: redirect URL will be added by workflow after detecting APP_AUTH_INVALID
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
const errorMessage = error instanceof Error ? error.message : String(error ?? "");
|
|
2162
|
+
return {
|
|
2163
|
+
output: null,
|
|
2164
|
+
billing: { credits: 0 },
|
|
2165
|
+
meta: {
|
|
2166
|
+
success: false,
|
|
2167
|
+
message: errorMessage,
|
|
2168
|
+
toolName
|
|
2169
|
+
},
|
|
2170
|
+
error: {
|
|
2171
|
+
code: "TOOL_EXECUTION_ERROR",
|
|
2172
|
+
message: errorMessage
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
} finally {
|
|
2176
|
+
process.env = originalEnv;
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/server/dedicated.ts
|
|
2182
|
+
import http from "http";
|
|
2183
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2184
|
+
|
|
2185
|
+
// src/server/core-api-handler.ts
|
|
2186
|
+
async function handleCoreMethod(method, params) {
|
|
2187
|
+
const service = coreApiService.getService();
|
|
2188
|
+
if (!service) {
|
|
2189
|
+
return {
|
|
2190
|
+
status: 404,
|
|
2191
|
+
payload: { error: "Core API service not configured" }
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
if (method === "createCommunicationChannel") {
|
|
2195
|
+
if (!params?.channel) {
|
|
2196
|
+
return { status: 400, payload: { error: "channel is required" } };
|
|
2197
|
+
}
|
|
2198
|
+
const channel = params.channel;
|
|
2199
|
+
const result = await coreApiService.callCreateChannel(channel);
|
|
2200
|
+
if (!result) {
|
|
2201
|
+
return {
|
|
2202
|
+
status: 500,
|
|
2203
|
+
payload: { error: "Core API service did not respond" }
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
return { status: 200, payload: result };
|
|
2207
|
+
}
|
|
2208
|
+
if (method === "updateCommunicationChannel") {
|
|
2209
|
+
if (!params?.channel) {
|
|
2210
|
+
return { status: 400, payload: { error: "channel is required" } };
|
|
2211
|
+
}
|
|
2212
|
+
const channel = params.channel;
|
|
2213
|
+
const result = await coreApiService.callUpdateChannel(channel);
|
|
2214
|
+
if (!result) {
|
|
2215
|
+
return {
|
|
2216
|
+
status: 500,
|
|
2217
|
+
payload: { error: "Core API service did not respond" }
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
return { status: 200, payload: result };
|
|
2221
|
+
}
|
|
2222
|
+
if (method === "deleteCommunicationChannel") {
|
|
2223
|
+
if (!params?.id || typeof params.id !== "string") {
|
|
2224
|
+
return { status: 400, payload: { error: "id is required" } };
|
|
2225
|
+
}
|
|
2226
|
+
const result = await coreApiService.callDeleteChannel(params.id);
|
|
2227
|
+
if (!result) {
|
|
2228
|
+
return {
|
|
2229
|
+
status: 500,
|
|
2230
|
+
payload: { error: "Core API service did not respond" }
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
return { status: 200, payload: result };
|
|
2234
|
+
}
|
|
2235
|
+
if (method === "getCommunicationChannel") {
|
|
2236
|
+
if (!params?.id || typeof params.id !== "string") {
|
|
2237
|
+
return { status: 400, payload: { error: "id is required" } };
|
|
2238
|
+
}
|
|
2239
|
+
const result = await coreApiService.callGetChannel(params.id);
|
|
2240
|
+
if (!result) {
|
|
2241
|
+
return {
|
|
2242
|
+
status: 404,
|
|
2243
|
+
payload: { error: "Channel not found" }
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
return { status: 200, payload: result };
|
|
2247
|
+
}
|
|
2248
|
+
if (method === "getCommunicationChannels") {
|
|
2249
|
+
const result = await coreApiService.callListChannels();
|
|
2250
|
+
if (!result) {
|
|
2251
|
+
return {
|
|
2252
|
+
status: 500,
|
|
2253
|
+
payload: { error: "Core API service did not respond" }
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
return { status: 200, payload: result };
|
|
2257
|
+
}
|
|
2258
|
+
if (method === "communicationChannel.list") {
|
|
2259
|
+
const result = await coreApiService.callListChannels();
|
|
2260
|
+
if (!result) {
|
|
2261
|
+
return {
|
|
2262
|
+
status: 500,
|
|
2263
|
+
payload: { error: "Core API service did not respond" }
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
return { status: 200, payload: result };
|
|
2267
|
+
}
|
|
2268
|
+
if (method === "communicationChannel.get") {
|
|
2269
|
+
if (!params?.id || typeof params.id !== "string") {
|
|
2270
|
+
return { status: 400, payload: { error: "id is required" } };
|
|
2271
|
+
}
|
|
2272
|
+
const result = await coreApiService.callGetChannel(params.id);
|
|
2273
|
+
if (!result) {
|
|
2274
|
+
return {
|
|
2275
|
+
status: 404,
|
|
2276
|
+
payload: { error: "Channel not found" }
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
return { status: 200, payload: result };
|
|
2280
|
+
}
|
|
2281
|
+
if (method === "workplace.list") {
|
|
2282
|
+
const result = await coreApiService.callListWorkplaces();
|
|
2283
|
+
if (!result) {
|
|
2284
|
+
return {
|
|
2285
|
+
status: 500,
|
|
2286
|
+
payload: { error: "Core API service did not respond" }
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
return { status: 200, payload: result };
|
|
2290
|
+
}
|
|
2291
|
+
if (method === "workplace.get") {
|
|
2292
|
+
if (!params?.id || typeof params.id !== "string") {
|
|
2293
|
+
return { status: 400, payload: { error: "id is required" } };
|
|
2294
|
+
}
|
|
2295
|
+
const result = await coreApiService.callGetWorkplace(params.id);
|
|
2296
|
+
if (!result) {
|
|
2297
|
+
return {
|
|
2298
|
+
status: 404,
|
|
2299
|
+
payload: { error: "Workplace not found" }
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
return { status: 200, payload: result };
|
|
2303
|
+
}
|
|
2304
|
+
if (method === "sendMessage") {
|
|
2305
|
+
if (!params?.message || !params?.communicationChannel) {
|
|
2306
|
+
return { status: 400, payload: { error: "message and communicationChannel are required" } };
|
|
2307
|
+
}
|
|
2308
|
+
const msg = params.message;
|
|
2309
|
+
const channel = params.communicationChannel;
|
|
2310
|
+
const result = await coreApiService.callSendMessage({
|
|
2311
|
+
message: msg,
|
|
2312
|
+
communicationChannel: channel
|
|
2313
|
+
});
|
|
2314
|
+
if (!result) {
|
|
2315
|
+
return {
|
|
2316
|
+
status: 500,
|
|
2317
|
+
payload: { error: "Core API service did not respond" }
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
return { status: 200, payload: result };
|
|
2321
|
+
}
|
|
2322
|
+
return {
|
|
2323
|
+
status: 400,
|
|
2324
|
+
payload: { error: "Unknown core method" }
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// src/server/handler-helpers.ts
|
|
2329
|
+
function parseHandlerEnvelope(parsedBody) {
|
|
2330
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
|
|
2331
|
+
return null;
|
|
2332
|
+
}
|
|
2333
|
+
const envelope = parsedBody;
|
|
2334
|
+
if (typeof envelope.env !== "object" || envelope.env === null || Array.isArray(envelope.env)) {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
if (typeof envelope.request !== "object" || envelope.request === null || Array.isArray(envelope.request)) {
|
|
2338
|
+
return null;
|
|
2339
|
+
}
|
|
2340
|
+
return {
|
|
2341
|
+
env: envelope.env,
|
|
2342
|
+
request: envelope.request,
|
|
2343
|
+
context: envelope.context
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
function buildRequestFromRaw(raw) {
|
|
2347
|
+
let parsedBody = raw.body;
|
|
2348
|
+
const contentType = raw.headers["content-type"] ?? "";
|
|
2349
|
+
if (contentType.includes("application/json")) {
|
|
2350
|
+
try {
|
|
2351
|
+
parsedBody = raw.body ? JSON.parse(raw.body) : {};
|
|
2352
|
+
} catch {
|
|
2353
|
+
parsedBody = raw.body;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return {
|
|
2357
|
+
method: raw.method,
|
|
2358
|
+
url: raw.url,
|
|
2359
|
+
path: raw.path,
|
|
2360
|
+
headers: raw.headers,
|
|
2361
|
+
query: raw.query,
|
|
2362
|
+
body: parsedBody,
|
|
2363
|
+
rawBody: raw.body ? Buffer.from(raw.body, "utf-8") : void 0
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
function buildRequestScopedConfig(env) {
|
|
2367
|
+
return {
|
|
2368
|
+
baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
2369
|
+
apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// src/server/startup-logger.ts
|
|
2374
|
+
function padEnd(str, length) {
|
|
2375
|
+
if (str.length >= length) {
|
|
2376
|
+
return str.slice(0, length);
|
|
2377
|
+
}
|
|
2378
|
+
return str + " ".repeat(length - str.length);
|
|
2379
|
+
}
|
|
2380
|
+
function printStartupLog(config, tools, webhookRegistry, port) {
|
|
2381
|
+
if (process.env.NODE_ENV === "test") {
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
const webhookCount = webhookRegistry ? Object.keys(webhookRegistry).length : 0;
|
|
2385
|
+
const webhookNames = webhookRegistry ? Object.keys(webhookRegistry) : [];
|
|
2386
|
+
const maxRequests = config.maxRequests ?? parseNumberEnv(process.env.MCP_MAX_REQUESTS) ?? null;
|
|
2387
|
+
const ttlExtendSeconds = config.ttlExtendSeconds ?? parseNumberEnv(process.env.MCP_TTL_EXTEND) ?? 3600;
|
|
2388
|
+
const executableId = process.env.SKEDYUL_EXECUTABLE_ID || "local";
|
|
2389
|
+
const divider = "\u2550".repeat(70);
|
|
2390
|
+
const thinDivider = "\u2500".repeat(70);
|
|
2391
|
+
console.log("");
|
|
2392
|
+
console.log(`\u2554${divider}\u2557`);
|
|
2393
|
+
console.log(`\u2551 \u{1F680} Skedyul MCP Server Starting \u2551`);
|
|
2394
|
+
console.log(`\u2560${divider}\u2563`);
|
|
2395
|
+
console.log(`\u2551 \u2551`);
|
|
2396
|
+
console.log(`\u2551 \u{1F4E6} Server: ${padEnd(config.metadata.name, 49)}\u2551`);
|
|
2397
|
+
console.log(`\u2551 \u{1F3F7}\uFE0F Version: ${padEnd(config.metadata.version, 49)}\u2551`);
|
|
2398
|
+
console.log(`\u2551 \u26A1 Compute: ${padEnd(config.computeLayer, 49)}\u2551`);
|
|
2399
|
+
if (port) {
|
|
2400
|
+
console.log(`\u2551 \u{1F310} Port: ${padEnd(String(port), 49)}\u2551`);
|
|
2401
|
+
}
|
|
2402
|
+
console.log(`\u2551 \u{1F511} Executable: ${padEnd(executableId, 49)}\u2551`);
|
|
2403
|
+
console.log(`\u2551 \u2551`);
|
|
2404
|
+
console.log(`\u255F${thinDivider}\u2562`);
|
|
2405
|
+
console.log(`\u2551 \u2551`);
|
|
2406
|
+
console.log(`\u2551 \u{1F527} Tools (${tools.length}): \u2551`);
|
|
2407
|
+
const maxToolsToShow = 10;
|
|
2408
|
+
const toolsToShow = tools.slice(0, maxToolsToShow);
|
|
2409
|
+
for (const tool of toolsToShow) {
|
|
2410
|
+
console.log(`\u2551 \u2022 ${padEnd(tool.name, 61)}\u2551`);
|
|
2411
|
+
}
|
|
2412
|
+
if (tools.length > maxToolsToShow) {
|
|
2413
|
+
console.log(`\u2551 ... and ${tools.length - maxToolsToShow} more \u2551`);
|
|
2414
|
+
}
|
|
2415
|
+
if (webhookCount > 0) {
|
|
2416
|
+
console.log(`\u2551 \u2551`);
|
|
2417
|
+
console.log(`\u2551 \u{1FA9D} Webhooks (${webhookCount}): \u2551`);
|
|
2418
|
+
const maxWebhooksToShow = 5;
|
|
2419
|
+
const webhooksToShow = webhookNames.slice(0, maxWebhooksToShow);
|
|
2420
|
+
for (const name of webhooksToShow) {
|
|
2421
|
+
console.log(`\u2551 \u2022 /webhooks/${padEnd(name, 51)}\u2551`);
|
|
2422
|
+
}
|
|
2423
|
+
if (webhookCount > maxWebhooksToShow) {
|
|
2424
|
+
console.log(`\u2551 ... and ${webhookCount - maxWebhooksToShow} more \u2551`);
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
console.log(`\u2551 \u2551`);
|
|
2428
|
+
console.log(`\u255F${thinDivider}\u2562`);
|
|
2429
|
+
console.log(`\u2551 \u2551`);
|
|
2430
|
+
console.log(`\u2551 \u2699\uFE0F Configuration: \u2551`);
|
|
2431
|
+
console.log(`\u2551 Max Requests: ${padEnd(maxRequests !== null ? String(maxRequests) : "unlimited", 46)}\u2551`);
|
|
2432
|
+
console.log(`\u2551 TTL Extend: ${padEnd(`${ttlExtendSeconds}s`, 46)}\u2551`);
|
|
2433
|
+
console.log(`\u2551 \u2551`);
|
|
2434
|
+
console.log(`\u255F${thinDivider}\u2562`);
|
|
2435
|
+
console.log(`\u2551 \u2705 Ready at ${padEnd((/* @__PURE__ */ new Date()).toISOString(), 55)}\u2551`);
|
|
2436
|
+
console.log(`\u255A${divider}\u255D`);
|
|
2437
|
+
console.log("");
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/server/dedicated.ts
|
|
2441
|
+
function createDedicatedServerInstance(config, tools, callTool, state, mcpServer, webhookRegistry) {
|
|
2442
|
+
const port = getListeningPort(config);
|
|
2443
|
+
const httpServer = http.createServer(
|
|
2444
|
+
async (req, res) => {
|
|
2445
|
+
function sendCoreResult(result) {
|
|
2446
|
+
sendJSON(res, result.status, result.payload);
|
|
2447
|
+
}
|
|
2448
|
+
try {
|
|
2449
|
+
const url = new URL(
|
|
2450
|
+
req.url || "/",
|
|
2451
|
+
`http://${req.headers.host || "localhost"}`
|
|
2452
|
+
);
|
|
2453
|
+
const pathname = url.pathname;
|
|
2454
|
+
if (pathname === "/health" && req.method === "GET") {
|
|
2455
|
+
sendJSON(res, 200, state.getHealthStatus());
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
if (pathname.startsWith("/webhooks/") && webhookRegistry) {
|
|
2459
|
+
const handle = pathname.slice("/webhooks/".length);
|
|
2460
|
+
const webhookDef = webhookRegistry[handle];
|
|
2461
|
+
if (!webhookDef) {
|
|
2462
|
+
sendJSON(res, 404, { error: `Webhook handler '${handle}' not found` });
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
const allowedMethods = webhookDef.methods ?? ["POST"];
|
|
2466
|
+
if (!allowedMethods.includes(req.method)) {
|
|
2467
|
+
sendJSON(res, 405, { error: `Method ${req.method} not allowed` });
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
let rawBody;
|
|
2471
|
+
try {
|
|
2472
|
+
rawBody = await readRawRequestBody(req);
|
|
2473
|
+
} catch {
|
|
2474
|
+
sendJSON(res, 400, { error: "Failed to read request body" });
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
let parsedBody;
|
|
2478
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
2479
|
+
if (contentType.includes("application/json")) {
|
|
2480
|
+
try {
|
|
2481
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
2482
|
+
} catch {
|
|
2483
|
+
parsedBody = rawBody;
|
|
2484
|
+
}
|
|
2485
|
+
} else {
|
|
2486
|
+
parsedBody = rawBody;
|
|
2487
|
+
}
|
|
2488
|
+
const envelope = parseHandlerEnvelope(parsedBody);
|
|
2489
|
+
let webhookRequest;
|
|
2490
|
+
let webhookContext;
|
|
2491
|
+
let requestEnv = {};
|
|
2492
|
+
let invocation;
|
|
2493
|
+
if (envelope && "context" in envelope && envelope.context) {
|
|
2494
|
+
const context = envelope.context;
|
|
2495
|
+
requestEnv = envelope.env;
|
|
2496
|
+
invocation = parsedBody.invocation;
|
|
2497
|
+
webhookRequest = buildRequestFromRaw(envelope.request);
|
|
2498
|
+
const envVars = { ...process.env, ...envelope.env };
|
|
2499
|
+
const app = context.app;
|
|
2500
|
+
if (context.appInstallationId && context.workplace) {
|
|
2501
|
+
webhookContext = {
|
|
2502
|
+
env: envVars,
|
|
2503
|
+
app,
|
|
2504
|
+
appInstallationId: context.appInstallationId,
|
|
2505
|
+
workplace: context.workplace,
|
|
2506
|
+
registration: context.registration ?? {},
|
|
2507
|
+
invocation,
|
|
2508
|
+
log: createContextLogger()
|
|
2509
|
+
};
|
|
2510
|
+
} else {
|
|
2511
|
+
webhookContext = {
|
|
2512
|
+
env: envVars,
|
|
2513
|
+
app,
|
|
2514
|
+
invocation,
|
|
2515
|
+
log: createContextLogger()
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
} else {
|
|
2519
|
+
const appId = req.headers["x-skedyul-app-id"];
|
|
2520
|
+
const appVersionId = req.headers["x-skedyul-app-version-id"];
|
|
2521
|
+
if (!appId || !appVersionId) {
|
|
2522
|
+
throw new Error("Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)");
|
|
2523
|
+
}
|
|
2524
|
+
webhookRequest = {
|
|
2525
|
+
method: req.method ?? "POST",
|
|
2526
|
+
url: url.toString(),
|
|
2527
|
+
path: pathname,
|
|
2528
|
+
headers: req.headers,
|
|
2529
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
2530
|
+
body: parsedBody,
|
|
2531
|
+
rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
|
|
2532
|
+
};
|
|
2533
|
+
webhookContext = {
|
|
2534
|
+
env: process.env,
|
|
2535
|
+
app: { id: appId, versionId: appVersionId },
|
|
2536
|
+
log: createContextLogger()
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const originalEnv = { ...process.env };
|
|
2540
|
+
Object.assign(process.env, requestEnv);
|
|
2541
|
+
const requestConfig = buildRequestScopedConfig(requestEnv);
|
|
2542
|
+
let webhookResponse;
|
|
2543
|
+
try {
|
|
2544
|
+
webhookResponse = await runWithLogContext({ invocation }, async () => {
|
|
2545
|
+
return await runWithConfig(requestConfig, async () => {
|
|
2546
|
+
return await webhookDef.handler(webhookRequest, webhookContext);
|
|
2547
|
+
});
|
|
2548
|
+
});
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
console.error(`Webhook handler '${handle}' error:`, err);
|
|
2551
|
+
sendJSON(res, 500, { error: "Webhook handler error" });
|
|
2552
|
+
return;
|
|
2553
|
+
} finally {
|
|
2554
|
+
process.env = originalEnv;
|
|
2555
|
+
}
|
|
2556
|
+
const status = webhookResponse.status ?? 200;
|
|
2557
|
+
const responseHeaders = {
|
|
2558
|
+
...webhookResponse.headers
|
|
2559
|
+
};
|
|
2560
|
+
if (!responseHeaders["Content-Type"] && !responseHeaders["content-type"]) {
|
|
2561
|
+
responseHeaders["Content-Type"] = "application/json";
|
|
2562
|
+
}
|
|
2563
|
+
res.writeHead(status, responseHeaders);
|
|
2564
|
+
if (webhookResponse.body !== void 0) {
|
|
2565
|
+
if (typeof webhookResponse.body === "string") {
|
|
2566
|
+
res.end(webhookResponse.body);
|
|
2567
|
+
} else {
|
|
2568
|
+
res.end(JSON.stringify(webhookResponse.body));
|
|
2569
|
+
}
|
|
2570
|
+
} else {
|
|
2571
|
+
res.end();
|
|
2572
|
+
}
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (pathname === "/estimate" && req.method === "POST") {
|
|
2576
|
+
let estimateBody;
|
|
2577
|
+
try {
|
|
2578
|
+
estimateBody = await parseJSONBody(req);
|
|
2579
|
+
} catch {
|
|
2580
|
+
sendJSON(res, 400, {
|
|
2581
|
+
error: {
|
|
2582
|
+
code: -32700,
|
|
2583
|
+
message: "Parse error"
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
try {
|
|
2589
|
+
const estimateResponse = await callTool(estimateBody.name, {
|
|
2590
|
+
inputs: estimateBody.inputs,
|
|
2591
|
+
estimate: true
|
|
2592
|
+
});
|
|
2593
|
+
sendJSON(res, 200, {
|
|
2594
|
+
billing: estimateResponse.billing ?? { credits: 0 }
|
|
2595
|
+
});
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
sendJSON(res, 500, {
|
|
2598
|
+
error: {
|
|
2599
|
+
code: -32603,
|
|
2600
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
if (pathname === "/oauth_callback" && req.method === "POST") {
|
|
2607
|
+
if (!config.hooks?.oauth_callback) {
|
|
2608
|
+
sendJSON(res, 404, { error: "OAuth callback handler not configured" });
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
let parsedBody;
|
|
2612
|
+
try {
|
|
2613
|
+
parsedBody = await parseJSONBody(req);
|
|
2614
|
+
} catch (err) {
|
|
2615
|
+
console.error("[OAuth Callback] Failed to parse JSON body:", err);
|
|
2616
|
+
sendJSON(res, 400, {
|
|
2617
|
+
error: { code: -32700, message: "Parse error" }
|
|
2618
|
+
});
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
const envelope = parseHandlerEnvelope(parsedBody);
|
|
2622
|
+
if (!envelope) {
|
|
2623
|
+
console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
|
|
2624
|
+
sendJSON(res, 400, {
|
|
2625
|
+
error: { code: -32602, message: "Missing envelope format: expected { env, request }" }
|
|
2626
|
+
});
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const invocation = parsedBody.invocation;
|
|
2630
|
+
const oauthRequest = buildRequestFromRaw(envelope.request);
|
|
2631
|
+
const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
|
|
2632
|
+
const oauthCallbackContext = {
|
|
2633
|
+
request: oauthRequest,
|
|
2634
|
+
invocation,
|
|
2635
|
+
log: createContextLogger()
|
|
2636
|
+
};
|
|
2637
|
+
try {
|
|
2638
|
+
const oauthCallbackHook = config.hooks.oauth_callback;
|
|
2639
|
+
const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
|
|
2640
|
+
const result = await runWithLogContext({ invocation }, async () => {
|
|
2641
|
+
return await runWithConfig(oauthCallbackRequestConfig, async () => {
|
|
2642
|
+
return await oauthCallbackHandler(oauthCallbackContext);
|
|
2643
|
+
});
|
|
2644
|
+
});
|
|
2645
|
+
sendJSON(res, 200, {
|
|
2646
|
+
appInstallationId: result.appInstallationId,
|
|
2647
|
+
env: result.env ?? {}
|
|
2648
|
+
});
|
|
2649
|
+
} catch (err) {
|
|
2650
|
+
const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
|
|
2651
|
+
sendJSON(res, 500, {
|
|
2652
|
+
error: {
|
|
2653
|
+
code: -32603,
|
|
2654
|
+
message: errorMessage
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (pathname === "/install" && req.method === "POST") {
|
|
2661
|
+
if (!config.hooks?.install) {
|
|
2662
|
+
sendJSON(res, 404, { error: "Install handler not configured" });
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
let installBody;
|
|
2666
|
+
try {
|
|
2667
|
+
installBody = await parseJSONBody(req);
|
|
2668
|
+
} catch {
|
|
2669
|
+
sendJSON(res, 400, {
|
|
2670
|
+
error: { code: -32700, message: "Parse error" }
|
|
2671
|
+
});
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
|
|
2675
|
+
sendJSON(res, 400, {
|
|
2676
|
+
error: { code: -32602, message: "Missing context (appInstallationId and workplace required)" }
|
|
2677
|
+
});
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const installContext = {
|
|
2681
|
+
env: installBody.env ?? {},
|
|
2682
|
+
workplace: installBody.context.workplace,
|
|
2683
|
+
appInstallationId: installBody.context.appInstallationId,
|
|
2684
|
+
app: installBody.context.app,
|
|
2685
|
+
invocation: installBody.invocation,
|
|
2686
|
+
log: createContextLogger()
|
|
2687
|
+
};
|
|
2688
|
+
const installRequestConfig = {
|
|
2689
|
+
baseUrl: installBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
2690
|
+
apiToken: installBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
2691
|
+
};
|
|
2692
|
+
try {
|
|
2693
|
+
const installHook = config.hooks.install;
|
|
2694
|
+
const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
|
|
2695
|
+
const result = await runWithLogContext({ invocation: installBody.invocation }, async () => {
|
|
2696
|
+
return await runWithConfig(installRequestConfig, async () => {
|
|
2697
|
+
return await installHandler(installContext);
|
|
2698
|
+
});
|
|
2699
|
+
});
|
|
2700
|
+
sendJSON(res, 200, {
|
|
2701
|
+
env: result.env ?? {},
|
|
2702
|
+
redirect: result.redirect
|
|
2703
|
+
});
|
|
2704
|
+
} catch (err) {
|
|
2705
|
+
if (err instanceof InstallError) {
|
|
2706
|
+
sendJSON(res, 400, {
|
|
2707
|
+
error: {
|
|
2708
|
+
code: err.code,
|
|
2709
|
+
message: err.message,
|
|
2710
|
+
field: err.field
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
2713
|
+
} else {
|
|
2714
|
+
sendJSON(res, 500, {
|
|
2715
|
+
error: {
|
|
2716
|
+
code: -32603,
|
|
2717
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (pathname === "/uninstall" && req.method === "POST") {
|
|
2725
|
+
if (!config.hooks?.uninstall) {
|
|
2726
|
+
sendJSON(res, 404, { error: "Uninstall handler not configured" });
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
let uninstallBody;
|
|
2730
|
+
try {
|
|
2731
|
+
uninstallBody = await parseJSONBody(req);
|
|
2732
|
+
} catch {
|
|
2733
|
+
sendJSON(res, 400, {
|
|
2734
|
+
error: { code: -32700, message: "Parse error" }
|
|
2735
|
+
});
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (!uninstallBody.context?.appInstallationId || !uninstallBody.context?.workplace || !uninstallBody.context?.app) {
|
|
2739
|
+
sendJSON(res, 400, {
|
|
2740
|
+
error: {
|
|
2741
|
+
code: -32602,
|
|
2742
|
+
message: "Missing context (appInstallationId, workplace and app required)"
|
|
2743
|
+
}
|
|
2744
|
+
});
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const uninstallContext = {
|
|
2748
|
+
env: uninstallBody.env ?? {},
|
|
2749
|
+
workplace: uninstallBody.context.workplace,
|
|
2750
|
+
appInstallationId: uninstallBody.context.appInstallationId,
|
|
2751
|
+
app: uninstallBody.context.app,
|
|
2752
|
+
invocation: uninstallBody.invocation,
|
|
2753
|
+
log: createContextLogger()
|
|
2754
|
+
};
|
|
2755
|
+
const uninstallRequestConfig = {
|
|
2756
|
+
baseUrl: uninstallBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
2757
|
+
apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
2758
|
+
};
|
|
2759
|
+
try {
|
|
2760
|
+
const uninstallHook = config.hooks.uninstall;
|
|
2761
|
+
const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
|
|
2762
|
+
const result = await runWithLogContext({ invocation: uninstallBody.invocation }, async () => {
|
|
2763
|
+
return await runWithConfig(uninstallRequestConfig, async () => {
|
|
2764
|
+
return await uninstallHandlerFn(uninstallContext);
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
sendJSON(res, 200, {
|
|
2768
|
+
cleanedWebhookIds: result.cleanedWebhookIds ?? []
|
|
2769
|
+
});
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
sendJSON(res, 500, {
|
|
2772
|
+
error: {
|
|
2773
|
+
code: -32603,
|
|
2774
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
if (pathname === "/provision" && req.method === "POST") {
|
|
2781
|
+
if (!config.hooks?.provision) {
|
|
2782
|
+
sendJSON(res, 404, { error: "Provision handler not configured" });
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
let provisionBody;
|
|
2786
|
+
try {
|
|
2787
|
+
provisionBody = await parseJSONBody(req);
|
|
2788
|
+
} catch {
|
|
2789
|
+
sendJSON(res, 400, {
|
|
2790
|
+
error: { code: -32700, message: "Parse error" }
|
|
2791
|
+
});
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
if (!provisionBody.context?.app) {
|
|
2795
|
+
sendJSON(res, 400, {
|
|
2796
|
+
error: { code: -32602, message: "Missing context (app required)" }
|
|
2797
|
+
});
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
const mergedEnv = {};
|
|
2801
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
2802
|
+
if (value !== void 0) {
|
|
2803
|
+
mergedEnv[key] = value;
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
Object.assign(mergedEnv, provisionBody.env ?? {});
|
|
2807
|
+
const provisionContext = {
|
|
2808
|
+
env: mergedEnv,
|
|
2809
|
+
app: provisionBody.context.app,
|
|
2810
|
+
invocation: provisionBody.invocation,
|
|
2811
|
+
log: createContextLogger()
|
|
2812
|
+
};
|
|
2813
|
+
const provisionRequestConfig = {
|
|
2814
|
+
baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
|
|
2815
|
+
apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
|
|
2816
|
+
};
|
|
2817
|
+
try {
|
|
2818
|
+
const provisionHook = config.hooks.provision;
|
|
2819
|
+
const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
|
|
2820
|
+
const result = await runWithLogContext({ invocation: provisionBody.invocation }, async () => {
|
|
2821
|
+
return await runWithConfig(provisionRequestConfig, async () => {
|
|
2822
|
+
return await provisionHandler(provisionContext);
|
|
2823
|
+
});
|
|
2824
|
+
});
|
|
2825
|
+
sendJSON(res, 200, result);
|
|
2826
|
+
} catch (err) {
|
|
2827
|
+
sendJSON(res, 500, {
|
|
2828
|
+
error: {
|
|
2829
|
+
code: -32603,
|
|
2830
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
2831
|
+
}
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
if (pathname === "/core" && req.method === "POST") {
|
|
2837
|
+
let coreBody;
|
|
2838
|
+
try {
|
|
2839
|
+
coreBody = await parseJSONBody(req);
|
|
2840
|
+
} catch {
|
|
2841
|
+
sendJSON(res, 400, {
|
|
2842
|
+
error: {
|
|
2843
|
+
code: -32700,
|
|
2844
|
+
message: "Parse error"
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
if (!coreBody?.method) {
|
|
2850
|
+
sendJSON(res, 400, {
|
|
2851
|
+
error: {
|
|
2852
|
+
code: -32602,
|
|
2853
|
+
message: "Missing method"
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const method = coreBody.method;
|
|
2859
|
+
const result = await handleCoreMethod(method, coreBody.params);
|
|
2860
|
+
sendCoreResult(result);
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
if (pathname === "/core/webhook" && req.method === "POST") {
|
|
2864
|
+
let rawWebhookBody;
|
|
2865
|
+
try {
|
|
2866
|
+
rawWebhookBody = await readRawRequestBody(req);
|
|
2867
|
+
} catch {
|
|
2868
|
+
sendJSON(res, 400, { status: "parse-error" });
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
let webhookBody;
|
|
2872
|
+
try {
|
|
2873
|
+
webhookBody = rawWebhookBody ? JSON.parse(rawWebhookBody) : {};
|
|
2874
|
+
} catch {
|
|
2875
|
+
sendJSON(res, 400, { status: "parse-error" });
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const normalizedHeaders = Object.fromEntries(
|
|
2879
|
+
Object.entries(req.headers).map(([key, value]) => [
|
|
2880
|
+
key,
|
|
2881
|
+
typeof value === "string" ? value : value?.[0] ?? ""
|
|
2882
|
+
])
|
|
2883
|
+
);
|
|
2884
|
+
const coreWebhookRequest = {
|
|
2885
|
+
method: req.method ?? "POST",
|
|
2886
|
+
headers: normalizedHeaders,
|
|
2887
|
+
body: webhookBody,
|
|
2888
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
2889
|
+
url: url.toString(),
|
|
2890
|
+
path: url.pathname,
|
|
2891
|
+
rawBody: rawWebhookBody ? Buffer.from(rawWebhookBody, "utf-8") : void 0
|
|
2892
|
+
};
|
|
2893
|
+
const webhookResponse = await coreApiService.dispatchWebhook(
|
|
2894
|
+
coreWebhookRequest
|
|
2895
|
+
);
|
|
2896
|
+
res.writeHead(webhookResponse.status, {
|
|
2897
|
+
"Content-Type": "application/json"
|
|
2898
|
+
});
|
|
2899
|
+
res.end(JSON.stringify(webhookResponse.body ?? {}));
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
if (pathname === "/mcp" && req.method === "POST") {
|
|
2903
|
+
try {
|
|
2904
|
+
const body = await parseJSONBody(req);
|
|
2905
|
+
if (body?.method === "tools/list") {
|
|
2906
|
+
sendJSON(res, 200, {
|
|
2907
|
+
jsonrpc: "2.0",
|
|
2908
|
+
id: body.id ?? null,
|
|
2909
|
+
result: { tools }
|
|
2910
|
+
});
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
if (body?.method === "webhooks/list") {
|
|
2914
|
+
const webhooks = webhookRegistry ? Object.values(webhookRegistry).map((w) => ({
|
|
2915
|
+
name: w.name,
|
|
2916
|
+
description: w.description,
|
|
2917
|
+
methods: w.methods ?? ["POST"],
|
|
2918
|
+
type: w.type ?? "WEBHOOK"
|
|
2919
|
+
})) : [];
|
|
2920
|
+
sendJSON(res, 200, {
|
|
2921
|
+
jsonrpc: "2.0",
|
|
2922
|
+
id: body.id ?? null,
|
|
2923
|
+
result: { webhooks }
|
|
2924
|
+
});
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
const transport = new StreamableHTTPServerTransport({
|
|
2928
|
+
sessionIdGenerator: void 0,
|
|
2929
|
+
enableJsonResponse: true
|
|
2930
|
+
});
|
|
2931
|
+
res.on("close", () => {
|
|
2932
|
+
transport.close();
|
|
2933
|
+
});
|
|
2934
|
+
await mcpServer.connect(transport);
|
|
2935
|
+
await transport.handleRequest(req, res, body);
|
|
2936
|
+
} catch (err) {
|
|
2937
|
+
sendJSON(res, 500, {
|
|
2938
|
+
jsonrpc: "2.0",
|
|
2939
|
+
id: null,
|
|
2940
|
+
error: {
|
|
2941
|
+
code: -32603,
|
|
2942
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
}
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
sendJSON(res, 404, {
|
|
2949
|
+
jsonrpc: "2.0",
|
|
2950
|
+
id: null,
|
|
2951
|
+
error: {
|
|
2952
|
+
code: -32601,
|
|
2953
|
+
message: "Not Found"
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
} catch (err) {
|
|
2957
|
+
sendJSON(res, 500, {
|
|
2958
|
+
jsonrpc: "2.0",
|
|
2959
|
+
id: null,
|
|
2960
|
+
error: {
|
|
2961
|
+
code: -32603,
|
|
2962
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
);
|
|
2968
|
+
return {
|
|
2969
|
+
async listen(listenPort) {
|
|
2970
|
+
const finalPort = listenPort ?? port;
|
|
2971
|
+
return new Promise((resolve2, reject) => {
|
|
2972
|
+
httpServer.listen(finalPort, () => {
|
|
2973
|
+
printStartupLog(config, tools, webhookRegistry, finalPort);
|
|
2974
|
+
resolve2();
|
|
2975
|
+
});
|
|
2976
|
+
httpServer.once("error", reject);
|
|
2977
|
+
});
|
|
2978
|
+
},
|
|
2979
|
+
getHealthStatus: () => state.getHealthStatus()
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// src/server/serverless.ts
|
|
2984
|
+
function createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry) {
|
|
2985
|
+
const headers = getDefaultHeaders(config.cors);
|
|
2986
|
+
let hasLoggedStartup = false;
|
|
2987
|
+
return {
|
|
2988
|
+
async handler(event) {
|
|
2989
|
+
if (!hasLoggedStartup) {
|
|
2990
|
+
printStartupLog(config, tools, webhookRegistry);
|
|
2991
|
+
hasLoggedStartup = true;
|
|
2992
|
+
}
|
|
2993
|
+
try {
|
|
2994
|
+
const path2 = event.path || event.rawPath || "/";
|
|
2995
|
+
const method = event.httpMethod || event.requestContext?.http?.method || "POST";
|
|
2996
|
+
if (method === "OPTIONS") {
|
|
2997
|
+
return createResponse(200, { message: "OK" }, headers);
|
|
2998
|
+
}
|
|
2999
|
+
if (path2.startsWith("/webhooks/") && webhookRegistry) {
|
|
3000
|
+
const handle = path2.slice("/webhooks/".length);
|
|
3001
|
+
const webhookDef = webhookRegistry[handle];
|
|
3002
|
+
if (!webhookDef) {
|
|
3003
|
+
return createResponse(404, { error: `Webhook handler '${handle}' not found` }, headers);
|
|
3004
|
+
}
|
|
3005
|
+
const allowedMethods = webhookDef.methods ?? ["POST"];
|
|
3006
|
+
if (!allowedMethods.includes(method)) {
|
|
3007
|
+
return createResponse(405, { error: `Method ${method} not allowed` }, headers);
|
|
3008
|
+
}
|
|
3009
|
+
const rawBody = event.body ?? "";
|
|
3010
|
+
let parsedBody;
|
|
3011
|
+
const contentType = event.headers?.["content-type"] ?? event.headers?.["Content-Type"] ?? "";
|
|
3012
|
+
if (contentType.includes("application/json")) {
|
|
3013
|
+
try {
|
|
3014
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
3015
|
+
} catch {
|
|
3016
|
+
parsedBody = rawBody;
|
|
3017
|
+
}
|
|
3018
|
+
} else {
|
|
3019
|
+
parsedBody = rawBody;
|
|
3020
|
+
}
|
|
3021
|
+
const isEnvelope = typeof parsedBody === "object" && parsedBody !== null && "env" in parsedBody && "request" in parsedBody && "context" in parsedBody;
|
|
3022
|
+
let webhookRequest;
|
|
3023
|
+
let webhookContext;
|
|
3024
|
+
let requestEnv = {};
|
|
3025
|
+
let invocation;
|
|
3026
|
+
if (isEnvelope) {
|
|
3027
|
+
const envelope = parsedBody;
|
|
3028
|
+
requestEnv = envelope.env ?? {};
|
|
3029
|
+
invocation = envelope.invocation;
|
|
3030
|
+
let originalParsedBody = envelope.request.body;
|
|
3031
|
+
const originalContentType = envelope.request.headers["content-type"] ?? "";
|
|
3032
|
+
if (originalContentType.includes("application/json")) {
|
|
3033
|
+
try {
|
|
3034
|
+
originalParsedBody = envelope.request.body ? JSON.parse(envelope.request.body) : {};
|
|
3035
|
+
} catch {
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
webhookRequest = {
|
|
3039
|
+
method: envelope.request.method,
|
|
3040
|
+
url: envelope.request.url,
|
|
3041
|
+
path: envelope.request.path,
|
|
3042
|
+
headers: envelope.request.headers,
|
|
3043
|
+
query: envelope.request.query,
|
|
3044
|
+
body: originalParsedBody,
|
|
3045
|
+
rawBody: envelope.request.body ? Buffer.from(envelope.request.body, "utf-8") : void 0
|
|
3046
|
+
};
|
|
3047
|
+
const envVars = { ...process.env, ...requestEnv };
|
|
3048
|
+
const app = envelope.context.app;
|
|
3049
|
+
if (envelope.context.appInstallationId && envelope.context.workplace) {
|
|
3050
|
+
webhookContext = {
|
|
3051
|
+
env: envVars,
|
|
3052
|
+
app,
|
|
3053
|
+
appInstallationId: envelope.context.appInstallationId,
|
|
3054
|
+
workplace: envelope.context.workplace,
|
|
3055
|
+
registration: envelope.context.registration ?? {},
|
|
3056
|
+
invocation,
|
|
3057
|
+
log: createContextLogger()
|
|
3058
|
+
};
|
|
3059
|
+
} else {
|
|
3060
|
+
webhookContext = {
|
|
3061
|
+
env: envVars,
|
|
3062
|
+
app,
|
|
3063
|
+
invocation,
|
|
3064
|
+
log: createContextLogger()
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
} else {
|
|
3068
|
+
const appId = event.headers?.["x-skedyul-app-id"] ?? event.headers?.["X-Skedyul-App-Id"];
|
|
3069
|
+
const appVersionId = event.headers?.["x-skedyul-app-version-id"] ?? event.headers?.["X-Skedyul-App-Version-Id"];
|
|
3070
|
+
if (!appId || !appVersionId) {
|
|
3071
|
+
throw new Error("Missing app info in webhook request (x-skedyul-app-id and x-skedyul-app-version-id headers required)");
|
|
3072
|
+
}
|
|
3073
|
+
const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
|
|
3074
|
+
const protocol = forwardedProto ?? "https";
|
|
3075
|
+
const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
|
|
3076
|
+
const queryString = event.queryStringParameters ? "?" + new URLSearchParams(event.queryStringParameters).toString() : "";
|
|
3077
|
+
const webhookUrl = `${protocol}://${host}${path2}${queryString}`;
|
|
3078
|
+
webhookRequest = {
|
|
3079
|
+
method,
|
|
3080
|
+
url: webhookUrl,
|
|
3081
|
+
path: path2,
|
|
3082
|
+
headers: event.headers,
|
|
3083
|
+
query: event.queryStringParameters ?? {},
|
|
3084
|
+
body: parsedBody,
|
|
3085
|
+
rawBody: rawBody ? Buffer.from(rawBody, "utf-8") : void 0
|
|
3086
|
+
};
|
|
3087
|
+
webhookContext = {
|
|
3088
|
+
env: process.env,
|
|
3089
|
+
app: { id: appId, versionId: appVersionId },
|
|
3090
|
+
log: createContextLogger()
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
const originalEnv = { ...process.env };
|
|
3094
|
+
Object.assign(process.env, requestEnv);
|
|
3095
|
+
const requestConfig = {
|
|
3096
|
+
baseUrl: requestEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
3097
|
+
apiToken: requestEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
3098
|
+
};
|
|
3099
|
+
let webhookResponse;
|
|
3100
|
+
try {
|
|
3101
|
+
webhookResponse = await runWithLogContext({ invocation }, async () => {
|
|
3102
|
+
return await runWithConfig(requestConfig, async () => {
|
|
3103
|
+
return await webhookDef.handler(webhookRequest, webhookContext);
|
|
3104
|
+
});
|
|
3105
|
+
});
|
|
3106
|
+
} catch (err) {
|
|
3107
|
+
console.error(`Webhook handler '${handle}' error:`, err);
|
|
3108
|
+
return createResponse(500, { error: "Webhook handler error" }, headers);
|
|
3109
|
+
} finally {
|
|
3110
|
+
process.env = originalEnv;
|
|
3111
|
+
}
|
|
3112
|
+
const responseHeaders = {
|
|
3113
|
+
...headers,
|
|
3114
|
+
...webhookResponse.headers
|
|
3115
|
+
};
|
|
3116
|
+
const status = webhookResponse.status ?? 200;
|
|
3117
|
+
const body = webhookResponse.body;
|
|
3118
|
+
return {
|
|
3119
|
+
statusCode: status,
|
|
3120
|
+
headers: responseHeaders,
|
|
3121
|
+
body: body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : ""
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
if (path2 === "/core" && method === "POST") {
|
|
3125
|
+
let coreBody;
|
|
3126
|
+
try {
|
|
3127
|
+
coreBody = event.body ? JSON.parse(event.body) : {};
|
|
3128
|
+
} catch {
|
|
3129
|
+
return createResponse(
|
|
3130
|
+
400,
|
|
3131
|
+
{
|
|
3132
|
+
error: {
|
|
3133
|
+
code: -32700,
|
|
3134
|
+
message: "Parse error"
|
|
3135
|
+
}
|
|
3136
|
+
},
|
|
3137
|
+
headers
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
if (!coreBody?.method) {
|
|
3141
|
+
return createResponse(
|
|
3142
|
+
400,
|
|
3143
|
+
{
|
|
3144
|
+
error: {
|
|
3145
|
+
code: -32602,
|
|
3146
|
+
message: "Missing method"
|
|
3147
|
+
}
|
|
3148
|
+
},
|
|
3149
|
+
headers
|
|
3150
|
+
);
|
|
3151
|
+
}
|
|
3152
|
+
const coreMethod = coreBody.method;
|
|
3153
|
+
const result = await handleCoreMethod(coreMethod, coreBody.params);
|
|
3154
|
+
return createResponse(result.status, result.payload, headers);
|
|
3155
|
+
}
|
|
3156
|
+
if (path2 === "/core/webhook" && method === "POST") {
|
|
3157
|
+
const rawWebhookBody = event.body ?? "";
|
|
3158
|
+
let webhookBody;
|
|
3159
|
+
try {
|
|
3160
|
+
webhookBody = rawWebhookBody ? JSON.parse(rawWebhookBody) : {};
|
|
3161
|
+
} catch {
|
|
3162
|
+
return createResponse(
|
|
3163
|
+
400,
|
|
3164
|
+
{ status: "parse-error" },
|
|
3165
|
+
headers
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
const forwardedProto = event.headers?.["x-forwarded-proto"] ?? event.headers?.["X-Forwarded-Proto"];
|
|
3169
|
+
const protocol = forwardedProto ?? "https";
|
|
3170
|
+
const host = event.headers?.host ?? event.headers?.Host ?? "localhost";
|
|
3171
|
+
const webhookUrl = `${protocol}://${host}${path2}`;
|
|
3172
|
+
const coreWebhookRequest = {
|
|
3173
|
+
method,
|
|
3174
|
+
headers: event.headers ?? {},
|
|
3175
|
+
body: webhookBody,
|
|
3176
|
+
query: event.queryStringParameters ?? {},
|
|
3177
|
+
url: webhookUrl,
|
|
3178
|
+
path: path2,
|
|
3179
|
+
rawBody: rawWebhookBody ? Buffer.from(rawWebhookBody, "utf-8") : void 0
|
|
3180
|
+
};
|
|
3181
|
+
const webhookResponse = await coreApiService.dispatchWebhook(
|
|
3182
|
+
coreWebhookRequest
|
|
3183
|
+
);
|
|
3184
|
+
return createResponse(
|
|
3185
|
+
webhookResponse.status,
|
|
3186
|
+
webhookResponse.body ?? {},
|
|
3187
|
+
headers
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
if (path2 === "/estimate" && method === "POST") {
|
|
3191
|
+
let estimateBody;
|
|
3192
|
+
try {
|
|
3193
|
+
estimateBody = event.body ? JSON.parse(event.body) : {};
|
|
3194
|
+
} catch {
|
|
3195
|
+
return createResponse(
|
|
3196
|
+
400,
|
|
3197
|
+
{
|
|
3198
|
+
error: {
|
|
3199
|
+
code: -32700,
|
|
3200
|
+
message: "Parse error"
|
|
3201
|
+
}
|
|
3202
|
+
},
|
|
3203
|
+
headers
|
|
3204
|
+
);
|
|
3205
|
+
}
|
|
3206
|
+
try {
|
|
3207
|
+
const toolName = estimateBody.name;
|
|
3208
|
+
const toolArgs = estimateBody.inputs ?? {};
|
|
3209
|
+
let toolKey = null;
|
|
3210
|
+
let tool = null;
|
|
3211
|
+
for (const [key, t] of Object.entries(registry)) {
|
|
3212
|
+
if (t.name === toolName || key === toolName) {
|
|
3213
|
+
toolKey = key;
|
|
3214
|
+
tool = t;
|
|
3215
|
+
break;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (!tool || !toolKey) {
|
|
3219
|
+
return createResponse(
|
|
3220
|
+
400,
|
|
3221
|
+
{
|
|
3222
|
+
error: {
|
|
3223
|
+
code: -32602,
|
|
3224
|
+
message: `Tool "${toolName}" not found`
|
|
3225
|
+
}
|
|
3226
|
+
},
|
|
3227
|
+
headers
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
const inputSchema = getZodSchema(tool.inputSchema);
|
|
3231
|
+
const validatedArgs = inputSchema ? inputSchema.parse(toolArgs) : toolArgs;
|
|
3232
|
+
const estimateResponse = await callTool(toolKey, {
|
|
3233
|
+
inputs: validatedArgs,
|
|
3234
|
+
estimate: true
|
|
3235
|
+
});
|
|
3236
|
+
return createResponse(
|
|
3237
|
+
200,
|
|
3238
|
+
{
|
|
3239
|
+
billing: estimateResponse.billing ?? { credits: 0 }
|
|
3240
|
+
},
|
|
3241
|
+
headers
|
|
3242
|
+
);
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
return createResponse(
|
|
3245
|
+
500,
|
|
3246
|
+
{
|
|
3247
|
+
error: {
|
|
3248
|
+
code: -32603,
|
|
3249
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
3250
|
+
}
|
|
3251
|
+
},
|
|
3252
|
+
headers
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
if (path2 === "/install" && method === "POST") {
|
|
3257
|
+
if (!config.hooks?.install) {
|
|
3258
|
+
return createResponse(404, { error: "Install handler not configured" }, headers);
|
|
3259
|
+
}
|
|
3260
|
+
let installBody;
|
|
3261
|
+
try {
|
|
3262
|
+
installBody = event.body ? JSON.parse(event.body) : {};
|
|
3263
|
+
} catch {
|
|
3264
|
+
return createResponse(
|
|
3265
|
+
400,
|
|
3266
|
+
{ error: { code: -32700, message: "Parse error" } },
|
|
3267
|
+
headers
|
|
3268
|
+
);
|
|
3269
|
+
}
|
|
3270
|
+
if (!installBody.context?.appInstallationId || !installBody.context?.workplace) {
|
|
3271
|
+
return createResponse(
|
|
3272
|
+
400,
|
|
3273
|
+
{ error: { code: -32602, message: "Missing context (appInstallationId and workplace required)" } },
|
|
3274
|
+
headers
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
const installContext = {
|
|
3278
|
+
env: installBody.env ?? {},
|
|
3279
|
+
workplace: installBody.context.workplace,
|
|
3280
|
+
appInstallationId: installBody.context.appInstallationId,
|
|
3281
|
+
app: installBody.context.app,
|
|
3282
|
+
invocation: installBody.invocation,
|
|
3283
|
+
log: createContextLogger()
|
|
3284
|
+
};
|
|
3285
|
+
const installRequestConfig = {
|
|
3286
|
+
baseUrl: installBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
3287
|
+
apiToken: installBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
3288
|
+
};
|
|
3289
|
+
try {
|
|
3290
|
+
const installHook = config.hooks.install;
|
|
3291
|
+
const installHandler = typeof installHook === "function" ? installHook : installHook.handler;
|
|
3292
|
+
const result = await runWithLogContext({ invocation: installBody.invocation }, async () => {
|
|
3293
|
+
return await runWithConfig(installRequestConfig, async () => {
|
|
3294
|
+
return await installHandler(installContext);
|
|
3295
|
+
});
|
|
3296
|
+
});
|
|
3297
|
+
return createResponse(
|
|
3298
|
+
200,
|
|
3299
|
+
{ env: result.env ?? {}, redirect: result.redirect },
|
|
3300
|
+
headers
|
|
3301
|
+
);
|
|
3302
|
+
} catch (err) {
|
|
3303
|
+
if (err instanceof InstallError) {
|
|
3304
|
+
return createResponse(
|
|
3305
|
+
400,
|
|
3306
|
+
{
|
|
3307
|
+
error: {
|
|
3308
|
+
code: err.code,
|
|
3309
|
+
message: err.message,
|
|
3310
|
+
field: err.field
|
|
3311
|
+
}
|
|
3312
|
+
},
|
|
3313
|
+
headers
|
|
3314
|
+
);
|
|
3315
|
+
}
|
|
3316
|
+
return createResponse(
|
|
3317
|
+
500,
|
|
3318
|
+
{
|
|
3319
|
+
error: {
|
|
3320
|
+
code: -32603,
|
|
3321
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
3322
|
+
}
|
|
3323
|
+
},
|
|
3324
|
+
headers
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
if (path2 === "/uninstall" && method === "POST") {
|
|
3329
|
+
if (!config.hooks?.uninstall) {
|
|
3330
|
+
return createResponse(404, { error: "Uninstall handler not configured" }, headers);
|
|
3331
|
+
}
|
|
3332
|
+
let uninstallBody;
|
|
3333
|
+
try {
|
|
3334
|
+
uninstallBody = event.body ? JSON.parse(event.body) : {};
|
|
3335
|
+
} catch {
|
|
3336
|
+
return createResponse(
|
|
3337
|
+
400,
|
|
3338
|
+
{ error: { code: -32700, message: "Parse error" } },
|
|
3339
|
+
headers
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
if (!uninstallBody.context?.appInstallationId || !uninstallBody.context?.workplace || !uninstallBody.context?.app) {
|
|
3343
|
+
return createResponse(
|
|
3344
|
+
400,
|
|
3345
|
+
{
|
|
3346
|
+
error: {
|
|
3347
|
+
code: -32602,
|
|
3348
|
+
message: "Missing context (appInstallationId, workplace and app required)"
|
|
3349
|
+
}
|
|
3350
|
+
},
|
|
3351
|
+
headers
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
const uninstallContext = {
|
|
3355
|
+
env: uninstallBody.env ?? {},
|
|
3356
|
+
workplace: uninstallBody.context.workplace,
|
|
3357
|
+
appInstallationId: uninstallBody.context.appInstallationId,
|
|
3358
|
+
app: uninstallBody.context.app,
|
|
3359
|
+
invocation: uninstallBody.invocation,
|
|
3360
|
+
log: createContextLogger()
|
|
3361
|
+
};
|
|
3362
|
+
const uninstallRequestConfig = {
|
|
3363
|
+
baseUrl: uninstallBody.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
|
|
3364
|
+
apiToken: uninstallBody.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
|
|
3365
|
+
};
|
|
3366
|
+
try {
|
|
3367
|
+
const uninstallHook = config.hooks.uninstall;
|
|
3368
|
+
const uninstallHandlerFn = typeof uninstallHook === "function" ? uninstallHook : uninstallHook.handler;
|
|
3369
|
+
const result = await runWithLogContext({ invocation: uninstallBody.invocation }, async () => {
|
|
3370
|
+
return await runWithConfig(uninstallRequestConfig, async () => {
|
|
3371
|
+
return await uninstallHandlerFn(uninstallContext);
|
|
3372
|
+
});
|
|
3373
|
+
});
|
|
3374
|
+
return createResponse(
|
|
3375
|
+
200,
|
|
3376
|
+
{ cleanedWebhookIds: result.cleanedWebhookIds ?? [] },
|
|
3377
|
+
headers
|
|
3378
|
+
);
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
return createResponse(
|
|
3381
|
+
500,
|
|
3382
|
+
{
|
|
3383
|
+
error: {
|
|
3384
|
+
code: -32603,
|
|
3385
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
3386
|
+
}
|
|
3387
|
+
},
|
|
3388
|
+
headers
|
|
3389
|
+
);
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
if (path2 === "/provision" && method === "POST") {
|
|
3393
|
+
console.log("[serverless] /provision endpoint called");
|
|
3394
|
+
if (!config.hooks?.provision) {
|
|
3395
|
+
console.log("[serverless] No provision handler configured");
|
|
3396
|
+
return createResponse(404, { error: "Provision handler not configured" }, headers);
|
|
3397
|
+
}
|
|
3398
|
+
let provisionBody;
|
|
3399
|
+
try {
|
|
3400
|
+
provisionBody = event.body ? JSON.parse(event.body) : {};
|
|
3401
|
+
console.log("[serverless] Provision body parsed:", {
|
|
3402
|
+
hasEnv: !!provisionBody.env,
|
|
3403
|
+
hasContext: !!provisionBody.context,
|
|
3404
|
+
appId: provisionBody.context?.app?.id,
|
|
3405
|
+
versionId: provisionBody.context?.app?.versionId
|
|
3406
|
+
});
|
|
3407
|
+
} catch {
|
|
3408
|
+
console.log("[serverless] Failed to parse provision body");
|
|
3409
|
+
return createResponse(
|
|
3410
|
+
400,
|
|
3411
|
+
{ error: { code: -32700, message: "Parse error" } },
|
|
3412
|
+
headers
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
if (!provisionBody.context?.app) {
|
|
3416
|
+
console.log("[serverless] Missing app context in provision body");
|
|
3417
|
+
return createResponse(
|
|
3418
|
+
400,
|
|
3419
|
+
{ error: { code: -32602, message: "Missing context (app required)" } },
|
|
3420
|
+
headers
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
const mergedEnv = {};
|
|
3424
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
3425
|
+
if (value !== void 0) {
|
|
3426
|
+
mergedEnv[key] = value;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
Object.assign(mergedEnv, provisionBody.env ?? {});
|
|
3430
|
+
const provisionContext = {
|
|
3431
|
+
env: mergedEnv,
|
|
3432
|
+
app: provisionBody.context.app,
|
|
3433
|
+
invocation: provisionBody.invocation,
|
|
3434
|
+
log: createContextLogger()
|
|
3435
|
+
};
|
|
3436
|
+
const provisionRequestConfig = {
|
|
3437
|
+
baseUrl: mergedEnv.SKEDYUL_API_URL ?? "",
|
|
3438
|
+
apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? ""
|
|
3439
|
+
};
|
|
3440
|
+
console.log("[serverless] Calling provision handler...");
|
|
3441
|
+
try {
|
|
3442
|
+
const provisionHook = config.hooks.provision;
|
|
3443
|
+
const provisionHandler = typeof provisionHook === "function" ? provisionHook : provisionHook.handler;
|
|
3444
|
+
const result = await runWithLogContext({ invocation: provisionBody.invocation }, async () => {
|
|
3445
|
+
return await runWithConfig(provisionRequestConfig, async () => {
|
|
3446
|
+
return await provisionHandler(provisionContext);
|
|
3447
|
+
});
|
|
3448
|
+
});
|
|
3449
|
+
console.log("[serverless] Provision handler completed successfully");
|
|
3450
|
+
return createResponse(200, result, headers);
|
|
3451
|
+
} catch (err) {
|
|
3452
|
+
console.error("[serverless] Provision handler failed:", err instanceof Error ? err.message : String(err));
|
|
3453
|
+
return createResponse(
|
|
3454
|
+
500,
|
|
3455
|
+
{
|
|
3456
|
+
error: {
|
|
3457
|
+
code: -32603,
|
|
3458
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
3459
|
+
}
|
|
3460
|
+
},
|
|
3461
|
+
headers
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
if (path2 === "/oauth_callback" && method === "POST") {
|
|
3466
|
+
if (!config.hooks?.oauth_callback) {
|
|
3467
|
+
return createResponse(
|
|
3468
|
+
404,
|
|
3469
|
+
{ error: "OAuth callback handler not configured" },
|
|
3470
|
+
headers
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
let parsedBody;
|
|
3474
|
+
try {
|
|
3475
|
+
parsedBody = event.body ? JSON.parse(event.body) : {};
|
|
3476
|
+
} catch (err) {
|
|
3477
|
+
console.error("[OAuth Callback] Failed to parse JSON body:", err);
|
|
3478
|
+
return createResponse(
|
|
3479
|
+
400,
|
|
3480
|
+
{ error: { code: -32700, message: "Parse error" } },
|
|
3481
|
+
headers
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
3484
|
+
const envelope = parseHandlerEnvelope(parsedBody);
|
|
3485
|
+
if (!envelope) {
|
|
3486
|
+
console.error("[OAuth Callback] Failed to parse envelope. Body:", JSON.stringify(parsedBody, null, 2));
|
|
3487
|
+
return createResponse(
|
|
3488
|
+
400,
|
|
3489
|
+
{ error: { code: -32602, message: "Missing envelope format: expected { env, request }" } },
|
|
3490
|
+
headers
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
const invocation = parsedBody.invocation;
|
|
3494
|
+
const oauthRequest = buildRequestFromRaw(envelope.request);
|
|
3495
|
+
const oauthCallbackRequestConfig = buildRequestScopedConfig(envelope.env);
|
|
3496
|
+
const oauthCallbackContext = {
|
|
3497
|
+
request: oauthRequest,
|
|
3498
|
+
invocation,
|
|
3499
|
+
log: createContextLogger()
|
|
3500
|
+
};
|
|
3501
|
+
try {
|
|
3502
|
+
const oauthCallbackHook = config.hooks.oauth_callback;
|
|
3503
|
+
const oauthCallbackHandler = typeof oauthCallbackHook === "function" ? oauthCallbackHook : oauthCallbackHook.handler;
|
|
3504
|
+
const result = await runWithLogContext({ invocation }, async () => {
|
|
3505
|
+
return await runWithConfig(oauthCallbackRequestConfig, async () => {
|
|
3506
|
+
return await oauthCallbackHandler(oauthCallbackContext);
|
|
3507
|
+
});
|
|
3508
|
+
});
|
|
3509
|
+
return createResponse(
|
|
3510
|
+
200,
|
|
3511
|
+
{
|
|
3512
|
+
appInstallationId: result.appInstallationId,
|
|
3513
|
+
env: result.env ?? {}
|
|
3514
|
+
},
|
|
3515
|
+
headers
|
|
3516
|
+
);
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
const errorMessage = err instanceof Error ? err.message : String(err ?? "Unknown error");
|
|
3519
|
+
return createResponse(
|
|
3520
|
+
500,
|
|
3521
|
+
{
|
|
3522
|
+
error: {
|
|
3523
|
+
code: -32603,
|
|
3524
|
+
message: errorMessage
|
|
3525
|
+
}
|
|
3526
|
+
},
|
|
3527
|
+
headers
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
if (path2 === "/health" && method === "GET") {
|
|
3532
|
+
return createResponse(200, state.getHealthStatus(), headers);
|
|
3533
|
+
}
|
|
3534
|
+
if (path2 === "/mcp" && method === "POST") {
|
|
3535
|
+
let body;
|
|
3536
|
+
try {
|
|
3537
|
+
body = event.body ? JSON.parse(event.body) : {};
|
|
3538
|
+
} catch {
|
|
3539
|
+
return createResponse(
|
|
3540
|
+
400,
|
|
3541
|
+
{
|
|
3542
|
+
jsonrpc: "2.0",
|
|
3543
|
+
id: null,
|
|
3544
|
+
error: {
|
|
3545
|
+
code: -32700,
|
|
3546
|
+
message: "Parse error"
|
|
3547
|
+
}
|
|
3548
|
+
},
|
|
3549
|
+
headers
|
|
3550
|
+
);
|
|
3551
|
+
}
|
|
3552
|
+
try {
|
|
3553
|
+
const { jsonrpc, id, method: rpcMethod, params } = body;
|
|
3554
|
+
if (jsonrpc !== "2.0") {
|
|
3555
|
+
return createResponse(
|
|
3556
|
+
400,
|
|
3557
|
+
{
|
|
3558
|
+
jsonrpc: "2.0",
|
|
3559
|
+
id,
|
|
3560
|
+
error: {
|
|
3561
|
+
code: -32600,
|
|
3562
|
+
message: "Invalid Request"
|
|
3563
|
+
}
|
|
3564
|
+
},
|
|
3565
|
+
headers
|
|
3566
|
+
);
|
|
3567
|
+
}
|
|
3568
|
+
let result;
|
|
3569
|
+
if (rpcMethod === "tools/list") {
|
|
3570
|
+
result = { tools };
|
|
3571
|
+
} else if (rpcMethod === "tools/call") {
|
|
3572
|
+
const toolName = params?.name;
|
|
3573
|
+
const rawArgs = params?.arguments ?? {};
|
|
3574
|
+
const hasSkedyulFormat = "inputs" in rawArgs || "env" in rawArgs || "context" in rawArgs || "invocation" in rawArgs;
|
|
3575
|
+
const toolInputs = hasSkedyulFormat ? rawArgs.inputs ?? {} : rawArgs;
|
|
3576
|
+
const toolContext = hasSkedyulFormat ? rawArgs.context : void 0;
|
|
3577
|
+
const toolEnv = hasSkedyulFormat ? rawArgs.env : void 0;
|
|
3578
|
+
const toolInvocation = hasSkedyulFormat ? rawArgs.invocation : void 0;
|
|
3579
|
+
let toolKey = null;
|
|
3580
|
+
let tool = null;
|
|
3581
|
+
for (const [key, t] of Object.entries(registry)) {
|
|
3582
|
+
if (t.name === toolName || key === toolName) {
|
|
3583
|
+
toolKey = key;
|
|
3584
|
+
tool = t;
|
|
3585
|
+
break;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
if (!tool || !toolKey) {
|
|
3589
|
+
return createResponse(
|
|
3590
|
+
200,
|
|
3591
|
+
{
|
|
3592
|
+
jsonrpc: "2.0",
|
|
3593
|
+
id,
|
|
3594
|
+
error: {
|
|
3595
|
+
code: -32602,
|
|
3596
|
+
message: `Tool "${toolName}" not found`
|
|
3597
|
+
}
|
|
3598
|
+
},
|
|
3599
|
+
headers
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
try {
|
|
3603
|
+
const inputSchema = getZodSchema(tool.inputSchema);
|
|
3604
|
+
const outputSchema = getZodSchema(tool.outputSchema);
|
|
3605
|
+
const hasOutputSchema = Boolean(outputSchema);
|
|
3606
|
+
const validatedInputs = inputSchema ? inputSchema.parse(toolInputs) : toolInputs;
|
|
3607
|
+
const toolResult = await callTool(toolKey, {
|
|
3608
|
+
inputs: validatedInputs,
|
|
3609
|
+
context: toolContext,
|
|
3610
|
+
env: toolEnv,
|
|
3611
|
+
invocation: toolInvocation
|
|
3612
|
+
});
|
|
3613
|
+
if (toolResult.error) {
|
|
3614
|
+
const errorOutput = { error: toolResult.error };
|
|
3615
|
+
result = {
|
|
3616
|
+
content: [{ type: "text", text: JSON.stringify(errorOutput) }],
|
|
3617
|
+
// Don't provide structuredContent for error responses when tool has outputSchema
|
|
3618
|
+
// because the error response won't match the success schema and MCP SDK validates it
|
|
3619
|
+
structuredContent: hasOutputSchema ? void 0 : errorOutput,
|
|
3620
|
+
isError: true,
|
|
3621
|
+
billing: toolResult.billing
|
|
3622
|
+
};
|
|
3623
|
+
} else {
|
|
3624
|
+
const outputData = toolResult.output;
|
|
3625
|
+
let structuredContent;
|
|
3626
|
+
if (outputData) {
|
|
3627
|
+
structuredContent = { ...outputData, __effect: toolResult.effect };
|
|
3628
|
+
} else if (toolResult.effect) {
|
|
3629
|
+
structuredContent = { __effect: toolResult.effect };
|
|
3630
|
+
} else if (hasOutputSchema) {
|
|
3631
|
+
structuredContent = {};
|
|
3632
|
+
}
|
|
3633
|
+
result = {
|
|
3634
|
+
content: [{ type: "text", text: JSON.stringify(toolResult.output) }],
|
|
3635
|
+
structuredContent,
|
|
3636
|
+
billing: toolResult.billing
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
} catch (validationError) {
|
|
3640
|
+
return createResponse(
|
|
3641
|
+
200,
|
|
3642
|
+
{
|
|
3643
|
+
jsonrpc: "2.0",
|
|
3644
|
+
id,
|
|
3645
|
+
error: {
|
|
3646
|
+
code: -32602,
|
|
3647
|
+
message: validationError instanceof Error ? validationError.message : "Invalid arguments"
|
|
3648
|
+
}
|
|
3649
|
+
},
|
|
3650
|
+
headers
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
} else if (rpcMethod === "webhooks/list") {
|
|
3654
|
+
const webhooks = webhookRegistry ? Object.values(webhookRegistry).map((w) => ({
|
|
3655
|
+
name: w.name,
|
|
3656
|
+
description: w.description,
|
|
3657
|
+
methods: w.methods ?? ["POST"],
|
|
3658
|
+
type: w.type ?? "WEBHOOK"
|
|
3659
|
+
})) : [];
|
|
3660
|
+
result = { webhooks };
|
|
3661
|
+
} else {
|
|
3662
|
+
return createResponse(
|
|
3663
|
+
200,
|
|
3664
|
+
{
|
|
3665
|
+
jsonrpc: "2.0",
|
|
3666
|
+
id,
|
|
3667
|
+
error: {
|
|
3668
|
+
code: -32601,
|
|
3669
|
+
message: `Method not found: ${rpcMethod}`
|
|
3670
|
+
}
|
|
3671
|
+
},
|
|
3672
|
+
headers
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
return createResponse(
|
|
3676
|
+
200,
|
|
3677
|
+
{
|
|
3678
|
+
jsonrpc: "2.0",
|
|
3679
|
+
id,
|
|
3680
|
+
result
|
|
3681
|
+
},
|
|
3682
|
+
headers
|
|
3683
|
+
);
|
|
3684
|
+
} catch (err) {
|
|
3685
|
+
return createResponse(
|
|
3686
|
+
500,
|
|
3687
|
+
{
|
|
3688
|
+
jsonrpc: "2.0",
|
|
3689
|
+
id: body?.id ?? null,
|
|
3690
|
+
error: {
|
|
3691
|
+
code: -32603,
|
|
3692
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
3693
|
+
}
|
|
3694
|
+
},
|
|
3695
|
+
headers
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
return createResponse(
|
|
3700
|
+
404,
|
|
3701
|
+
{
|
|
3702
|
+
jsonrpc: "2.0",
|
|
3703
|
+
id: null,
|
|
3704
|
+
error: {
|
|
3705
|
+
code: -32601,
|
|
3706
|
+
message: "Not Found"
|
|
3707
|
+
}
|
|
3708
|
+
},
|
|
3709
|
+
headers
|
|
3710
|
+
);
|
|
3711
|
+
} catch (err) {
|
|
3712
|
+
return createResponse(
|
|
3713
|
+
500,
|
|
3714
|
+
{
|
|
3715
|
+
jsonrpc: "2.0",
|
|
3716
|
+
id: null,
|
|
3717
|
+
error: {
|
|
3718
|
+
code: -32603,
|
|
3719
|
+
message: err instanceof Error ? err.message : String(err ?? "")
|
|
3720
|
+
}
|
|
3721
|
+
},
|
|
3722
|
+
headers
|
|
3723
|
+
);
|
|
3724
|
+
}
|
|
3725
|
+
},
|
|
3726
|
+
getHealthStatus: () => state.getHealthStatus()
|
|
3727
|
+
};
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// src/server/index.ts
|
|
3731
|
+
console.log("[skedyul-node/server] Module loading - imports done");
|
|
3732
|
+
console.log("[skedyul-node/server] All imports complete");
|
|
3733
|
+
console.log("[skedyul-node/server] Installing context logger...");
|
|
3734
|
+
installContextLogger();
|
|
3735
|
+
console.log("[skedyul-node/server] Context logger installed");
|
|
3736
|
+
function createSkedyulServer(config, registry, webhookRegistry) {
|
|
3737
|
+
console.log("[createSkedyulServer] Step 1: mergeRuntimeEnv()");
|
|
3738
|
+
mergeRuntimeEnv();
|
|
3739
|
+
console.log("[createSkedyulServer] Step 2: coreApi setup");
|
|
3740
|
+
if (config.coreApi?.service) {
|
|
3741
|
+
coreApiService.register(config.coreApi.service);
|
|
3742
|
+
if (config.coreApi.webhookHandler) {
|
|
3743
|
+
coreApiService.setWebhookHandler(config.coreApi.webhookHandler);
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
console.log("[createSkedyulServer] Step 3: buildToolMetadata()");
|
|
3747
|
+
const tools = buildToolMetadata(registry);
|
|
3748
|
+
console.log("[createSkedyulServer] Step 3 done, tools:", tools.length);
|
|
3749
|
+
const toolNames = Object.values(registry).map((tool) => tool.name);
|
|
3750
|
+
const runtimeLabel = config.computeLayer;
|
|
3751
|
+
const maxRequests = config.maxRequests ?? parseNumberEnv(process.env.MCP_MAX_REQUESTS) ?? null;
|
|
3752
|
+
const ttlExtendSeconds = config.ttlExtendSeconds ?? parseNumberEnv(process.env.MCP_TTL_EXTEND) ?? 3600;
|
|
3753
|
+
console.log("[createSkedyulServer] Step 4: createRequestState()");
|
|
3754
|
+
const state = createRequestState(
|
|
3755
|
+
maxRequests,
|
|
3756
|
+
ttlExtendSeconds,
|
|
3757
|
+
runtimeLabel,
|
|
3758
|
+
toolNames
|
|
3759
|
+
);
|
|
3760
|
+
console.log("[createSkedyulServer] Step 4 done");
|
|
3761
|
+
console.log("[createSkedyulServer] Step 5: new McpServer()");
|
|
3762
|
+
const mcpServer = new McpServer({
|
|
3763
|
+
name: config.metadata.name,
|
|
3764
|
+
version: config.metadata.version
|
|
3765
|
+
});
|
|
3766
|
+
console.log("[createSkedyulServer] Step 5 done");
|
|
3767
|
+
const dedicatedShutdown = () => {
|
|
3768
|
+
console.log("Max requests reached, shutting down...");
|
|
3769
|
+
setTimeout(() => process.exit(0), 1e3);
|
|
3770
|
+
};
|
|
3771
|
+
console.log("[createSkedyulServer] Step 6: createCallToolHandler()");
|
|
3772
|
+
const callTool = createCallToolHandler(
|
|
3773
|
+
registry,
|
|
3774
|
+
state,
|
|
3775
|
+
config.computeLayer === "dedicated" ? dedicatedShutdown : void 0
|
|
3776
|
+
);
|
|
3777
|
+
console.log("[createSkedyulServer] Step 6 done");
|
|
3778
|
+
console.log("[createSkedyulServer] Step 7: Registering tools...");
|
|
3779
|
+
for (const [toolKey, tool] of Object.entries(registry)) {
|
|
3780
|
+
console.log(`[createSkedyulServer] Registering tool: ${toolKey}`);
|
|
3781
|
+
const toolName = tool.name || toolKey;
|
|
3782
|
+
const toolDisplayName = tool.label || toolName;
|
|
3783
|
+
console.log(`[createSkedyulServer] Getting input schema for ${toolKey}`);
|
|
3784
|
+
const inputZodSchema = getZodSchema(tool.inputSchema);
|
|
3785
|
+
console.log(`[createSkedyulServer] Getting output schema for ${toolKey}`);
|
|
3786
|
+
const outputZodSchema = getZodSchema(tool.outputSchema);
|
|
3787
|
+
console.log(`[createSkedyulServer] Creating wrapped schema for ${toolKey}`);
|
|
3788
|
+
const wrappedInputSchema = z5.object({
|
|
3789
|
+
inputs: inputZodSchema ?? z5.record(z5.string(), z5.unknown()).optional(),
|
|
3790
|
+
env: z5.record(z5.string(), z5.string()).optional()
|
|
3791
|
+
}).passthrough();
|
|
3792
|
+
console.log(`[createSkedyulServer] Calling mcpServer.registerTool for ${toolKey}`);
|
|
3793
|
+
mcpServer.registerTool(
|
|
3794
|
+
toolName,
|
|
3795
|
+
{
|
|
3796
|
+
title: toolDisplayName,
|
|
3797
|
+
description: tool.description,
|
|
3798
|
+
inputSchema: wrappedInputSchema
|
|
3799
|
+
// Don't pass outputSchema to MCP SDK - it validates structuredContent against it
|
|
3800
|
+
// which fails for error responses. We handle output formatting ourselves.
|
|
3801
|
+
// outputSchema: outputZodSchema,
|
|
3802
|
+
},
|
|
3803
|
+
async (args) => {
|
|
3804
|
+
const rawArgs = args;
|
|
3805
|
+
const toolInputs = rawArgs.inputs ?? {};
|
|
3806
|
+
const toolContext = rawArgs.context;
|
|
3807
|
+
const toolEnv = rawArgs.env;
|
|
3808
|
+
const toolInvocation = rawArgs.invocation;
|
|
3809
|
+
let validatedInputs = toolInputs;
|
|
3810
|
+
if (inputZodSchema) {
|
|
3811
|
+
try {
|
|
3812
|
+
validatedInputs = inputZodSchema.parse(toolInputs);
|
|
3813
|
+
} catch (error) {
|
|
3814
|
+
console.error(
|
|
3815
|
+
`[registerTool] Input validation failed for tool ${toolName}:`,
|
|
3816
|
+
error
|
|
3817
|
+
);
|
|
3818
|
+
return {
|
|
3819
|
+
content: [
|
|
3820
|
+
{
|
|
3821
|
+
type: "text",
|
|
3822
|
+
text: JSON.stringify({
|
|
3823
|
+
error: `Input validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3824
|
+
})
|
|
3825
|
+
}
|
|
3826
|
+
],
|
|
3827
|
+
structuredContent: {
|
|
3828
|
+
error: `Input validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3829
|
+
},
|
|
3830
|
+
isError: true,
|
|
3831
|
+
billing: { credits: 0 }
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
const result = await callTool(toolKey, {
|
|
3836
|
+
inputs: validatedInputs,
|
|
3837
|
+
context: toolContext,
|
|
3838
|
+
env: toolEnv,
|
|
3839
|
+
invocation: toolInvocation
|
|
3840
|
+
});
|
|
3841
|
+
const hasOutputSchema = Boolean(outputZodSchema);
|
|
3842
|
+
if (result.error) {
|
|
3843
|
+
const errorOutput = { error: result.error };
|
|
3844
|
+
return {
|
|
3845
|
+
content: [{ type: "text", text: JSON.stringify(errorOutput) }],
|
|
3846
|
+
// Don't provide structuredContent for error responses when tool has outputSchema
|
|
3847
|
+
// because the error response won't match the success schema and MCP SDK validates it
|
|
3848
|
+
structuredContent: hasOutputSchema ? void 0 : errorOutput,
|
|
3849
|
+
isError: true,
|
|
3850
|
+
billing: result.billing
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
const outputData = result.output;
|
|
3854
|
+
let structuredContent;
|
|
3855
|
+
if (outputData) {
|
|
3856
|
+
structuredContent = { ...outputData, __effect: result.effect };
|
|
3857
|
+
} else if (result.effect) {
|
|
3858
|
+
structuredContent = { __effect: result.effect };
|
|
3859
|
+
} else if (hasOutputSchema) {
|
|
3860
|
+
structuredContent = {};
|
|
3861
|
+
}
|
|
3862
|
+
return {
|
|
3863
|
+
content: [{ type: "text", text: JSON.stringify(result.output) }],
|
|
3864
|
+
structuredContent,
|
|
3865
|
+
billing: result.billing
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
);
|
|
3869
|
+
console.log(`[createSkedyulServer] Tool ${toolKey} registered successfully`);
|
|
3870
|
+
}
|
|
3871
|
+
console.log("[createSkedyulServer] Step 7 done - all tools registered");
|
|
3872
|
+
console.log("[createSkedyulServer] Step 8: Creating server instance");
|
|
3873
|
+
if (config.computeLayer === "dedicated") {
|
|
3874
|
+
console.log("[createSkedyulServer] Creating dedicated instance");
|
|
3875
|
+
return createDedicatedServerInstance(
|
|
3876
|
+
config,
|
|
3877
|
+
tools,
|
|
3878
|
+
callTool,
|
|
3879
|
+
state,
|
|
3880
|
+
mcpServer,
|
|
3881
|
+
webhookRegistry
|
|
3882
|
+
);
|
|
3883
|
+
}
|
|
3884
|
+
console.log("[createSkedyulServer] Creating serverless instance");
|
|
3885
|
+
const serverlessInstance = createServerlessInstance(config, tools, callTool, state, mcpServer, registry, webhookRegistry);
|
|
3886
|
+
console.log("[createSkedyulServer] Serverless instance created successfully");
|
|
3887
|
+
return serverlessInstance;
|
|
3888
|
+
}
|
|
3889
|
+
var server = {
|
|
3890
|
+
create: createSkedyulServer
|
|
3891
|
+
};
|
|
3892
|
+
|
|
3893
|
+
// src/dockerfile.ts
|
|
3894
|
+
var DEFAULT_DOCKERFILE = `# =============================================================================
|
|
3895
|
+
# BUILDER STAGE - Common build for all targets
|
|
3896
|
+
# =============================================================================
|
|
3897
|
+
FROM public.ecr.aws/docker/library/node:22-alpine AS builder
|
|
3898
|
+
|
|
3899
|
+
ARG COMPUTE_LAYER=serverless
|
|
3900
|
+
ARG BUILD_EXTERNAL=""
|
|
3901
|
+
WORKDIR /app
|
|
3902
|
+
|
|
3903
|
+
# Install pnpm
|
|
3904
|
+
RUN corepack enable && corepack prepare pnpm@latest --activate
|
|
3905
|
+
|
|
3906
|
+
# Copy package files (lockfile is optional)
|
|
3907
|
+
COPY package.json tsconfig.json skedyul.config.ts ./
|
|
3908
|
+
COPY src ./src
|
|
3909
|
+
|
|
3910
|
+
# Copy tsup.config.ts if it exists, otherwise generate based on COMPUTE_LAYER
|
|
3911
|
+
# BUILD_EXTERNAL is a comma-separated list of additional externals (e.g., "twilio,stripe")
|
|
3912
|
+
COPY tsup.config.t[s] ./
|
|
3913
|
+
RUN if [ ! -f tsup.config.ts ]; then \\
|
|
3914
|
+
BASE_EXT="skedyul,zod"; \\
|
|
3915
|
+
if [ "$COMPUTE_LAYER" = "serverless" ]; then \\
|
|
3916
|
+
BASE_EXT="skedyul,skedyul/serverless,zod"; \\
|
|
3917
|
+
FORMAT="esm"; \\
|
|
3918
|
+
else \\
|
|
3919
|
+
BASE_EXT="skedyul,skedyul/dedicated,zod"; \\
|
|
3920
|
+
FORMAT="cjs"; \\
|
|
3921
|
+
fi; \\
|
|
3922
|
+
if [ -n "$BUILD_EXTERNAL" ]; then \\
|
|
3923
|
+
ALL_EXT="$BASE_EXT,$BUILD_EXTERNAL"; \\
|
|
3924
|
+
else \\
|
|
3925
|
+
ALL_EXT="$BASE_EXT"; \\
|
|
3926
|
+
fi; \\
|
|
3927
|
+
EXT_ARRAY=$(echo "$ALL_EXT" | sed 's/,/","/g'); \\
|
|
3928
|
+
printf 'import{defineConfig}from"tsup";export default defineConfig({entry:["src/server/mcp_server.ts"],format:["%s"],target:"node22",outDir:"dist/server",clean:true,splitting:false,dts:false,external:["%s"]})' "$FORMAT" "$EXT_ARRAY" > tsup.config.ts; \\
|
|
3929
|
+
fi
|
|
3930
|
+
|
|
3931
|
+
# Install dependencies (including dev deps for build), compile, smoke test, then prune
|
|
3932
|
+
# Note: Using --no-frozen-lockfile since lockfile may not exist
|
|
3933
|
+
# skedyul build reads computeLayer from skedyul.config.ts
|
|
3934
|
+
# Smoke test runs before pruning since skedyul CLI is a dev dependency
|
|
3935
|
+
RUN pnpm install --no-frozen-lockfile && \\
|
|
3936
|
+
pnpm run build && \\
|
|
3937
|
+
skedyul smoke-test && \\
|
|
3938
|
+
pnpm prune --prod && \\
|
|
3939
|
+
pnpm store prune && \\
|
|
3940
|
+
rm -rf /tmp/* /var/cache/apk/* ~/.npm
|
|
3941
|
+
|
|
3942
|
+
# =============================================================================
|
|
3943
|
+
# DEDICATED STAGE - For local Docker and ECS deployments (HTTP server)
|
|
3944
|
+
# =============================================================================
|
|
3945
|
+
FROM public.ecr.aws/docker/library/node:22-alpine AS dedicated
|
|
3946
|
+
|
|
3947
|
+
WORKDIR /app
|
|
3948
|
+
|
|
3949
|
+
# Copy built artifacts
|
|
3950
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
3951
|
+
COPY --from=builder /app/dist ./dist
|
|
3952
|
+
COPY --from=builder /app/package.json ./package.json
|
|
3953
|
+
|
|
3954
|
+
# Allow overriding the baked-in MCP env at runtime
|
|
3955
|
+
ARG MCP_ENV_JSON="{}"
|
|
3956
|
+
ENV MCP_ENV_JSON=\${MCP_ENV_JSON}
|
|
3957
|
+
|
|
3958
|
+
# Expose the HTTP port
|
|
3959
|
+
EXPOSE 3000
|
|
3960
|
+
|
|
3961
|
+
# Run as HTTP server (dedicated mode auto-detected by absence of AWS_LAMBDA_FUNCTION_NAME)
|
|
3962
|
+
CMD ["node", "dist/server/mcp_server.js"]
|
|
3963
|
+
|
|
3964
|
+
# =============================================================================
|
|
3965
|
+
# SERVERLESS STAGE - For AWS Lambda deployments
|
|
3966
|
+
# =============================================================================
|
|
3967
|
+
FROM public.ecr.aws/lambda/nodejs:22 AS serverless
|
|
3968
|
+
|
|
3969
|
+
WORKDIR \${LAMBDA_TASK_ROOT}
|
|
3970
|
+
|
|
3971
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
3972
|
+
COPY --from=builder /app/dist ./dist
|
|
3973
|
+
COPY --from=builder /app/package.json ./package.json
|
|
3974
|
+
|
|
3975
|
+
# Allow overriding the baked-in MCP env at runtime
|
|
3976
|
+
ARG MCP_ENV_JSON="{}"
|
|
3977
|
+
ENV MCP_ENV_JSON=\${MCP_ENV_JSON}
|
|
3978
|
+
|
|
3979
|
+
# Lambda handler format
|
|
3980
|
+
CMD ["dist/server/mcp_server.handler"]
|
|
3981
|
+
|
|
3982
|
+
# =============================================================================
|
|
3983
|
+
# DEFAULT - Use dedicated for local development, override with --target for production
|
|
3984
|
+
# =============================================================================
|
|
3985
|
+
FROM dedicated
|
|
3986
|
+
`;
|
|
3987
|
+
|
|
3988
|
+
// src/config/app-config.ts
|
|
3989
|
+
function defineConfig(config) {
|
|
3990
|
+
return config;
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
// src/config/define.ts
|
|
3994
|
+
function defineModel(model) {
|
|
3995
|
+
return model;
|
|
3996
|
+
}
|
|
3997
|
+
function defineChannel(channel) {
|
|
3998
|
+
return channel;
|
|
3999
|
+
}
|
|
4000
|
+
function definePage(page) {
|
|
4001
|
+
return page;
|
|
4002
|
+
}
|
|
4003
|
+
function defineWorkflow(workflow) {
|
|
4004
|
+
return workflow;
|
|
4005
|
+
}
|
|
4006
|
+
function defineAgent(agent) {
|
|
4007
|
+
return agent;
|
|
4008
|
+
}
|
|
4009
|
+
function defineEnv(env) {
|
|
4010
|
+
return env;
|
|
4011
|
+
}
|
|
4012
|
+
function defineNavigation(navigation) {
|
|
4013
|
+
return navigation;
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
// src/config/loader.ts
|
|
4017
|
+
import * as fs from "fs";
|
|
4018
|
+
import * as path from "path";
|
|
4019
|
+
import * as os from "os";
|
|
4020
|
+
var CONFIG_FILE_NAMES = [
|
|
4021
|
+
"skedyul.config.ts",
|
|
4022
|
+
"skedyul.config.js",
|
|
4023
|
+
"skedyul.config.mjs",
|
|
4024
|
+
"skedyul.config.cjs"
|
|
4025
|
+
];
|
|
4026
|
+
async function transpileTypeScript(filePath) {
|
|
4027
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
4028
|
+
const configDir = path.dirname(path.resolve(filePath));
|
|
4029
|
+
let transpiled = content.replace(/import\s+type\s+\{[^}]+\}\s+from\s+['"][^'"]+['"]\s*;?\n?/g, "").replace(/import\s+\{\s*defineConfig\s*\}\s+from\s+['"]skedyul['"]\s*;?\n?/g, "").replace(/:\s*SkedyulConfig/g, "").replace(/export\s+default\s+/, "module.exports = ").replace(/defineConfig\s*\(\s*\{/, "{").replace(/\}\s*\)\s*;?\s*$/, "}");
|
|
4030
|
+
transpiled = transpiled.replace(
|
|
4031
|
+
/import\s+(\w+)\s+from\s+['"](\.[^'"]+)['"]/g,
|
|
4032
|
+
(match, varName, relativePath) => {
|
|
4033
|
+
const absolutePath = path.resolve(configDir, relativePath);
|
|
4034
|
+
return `const ${varName} = require('${absolutePath.replace(/\\/g, "/")}')`;
|
|
4035
|
+
}
|
|
4036
|
+
);
|
|
4037
|
+
transpiled = transpiled.replace(/import\s*\(\s*['"][^'"]+['"]\s*\)/g, "null");
|
|
4038
|
+
return transpiled;
|
|
4039
|
+
}
|
|
4040
|
+
async function loadConfig(configPath) {
|
|
4041
|
+
const absolutePath = path.resolve(configPath);
|
|
4042
|
+
if (!fs.existsSync(absolutePath)) {
|
|
4043
|
+
throw new Error(`Config file not found: ${absolutePath}`);
|
|
4044
|
+
}
|
|
4045
|
+
const isTypeScript = absolutePath.endsWith(".ts");
|
|
4046
|
+
try {
|
|
4047
|
+
let moduleToLoad = absolutePath;
|
|
4048
|
+
if (isTypeScript) {
|
|
4049
|
+
const transpiled = await transpileTypeScript(absolutePath);
|
|
4050
|
+
const tempDir = os.tmpdir();
|
|
4051
|
+
const tempFile = path.join(tempDir, `skedyul-config-${Date.now()}.js`);
|
|
4052
|
+
fs.writeFileSync(tempFile, transpiled);
|
|
4053
|
+
moduleToLoad = tempFile;
|
|
4054
|
+
try {
|
|
4055
|
+
const module2 = __require(moduleToLoad);
|
|
4056
|
+
const config2 = module2.default || module2;
|
|
4057
|
+
if (!config2 || typeof config2 !== "object") {
|
|
4058
|
+
throw new Error("Config file must export a configuration object");
|
|
4059
|
+
}
|
|
4060
|
+
if (!config2.name || typeof config2.name !== "string") {
|
|
4061
|
+
throw new Error('Config must have a "name" property');
|
|
4062
|
+
}
|
|
4063
|
+
return config2;
|
|
4064
|
+
} finally {
|
|
4065
|
+
try {
|
|
4066
|
+
fs.unlinkSync(tempFile);
|
|
4067
|
+
} catch {
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
const module = await import(moduleToLoad);
|
|
4072
|
+
const config = module.default || module;
|
|
4073
|
+
if (!config || typeof config !== "object") {
|
|
4074
|
+
throw new Error("Config file must export a configuration object");
|
|
4075
|
+
}
|
|
4076
|
+
if (!config.name || typeof config.name !== "string") {
|
|
4077
|
+
throw new Error('Config must have a "name" property');
|
|
4078
|
+
}
|
|
4079
|
+
return config;
|
|
4080
|
+
} catch (error) {
|
|
4081
|
+
throw new Error(
|
|
4082
|
+
`Failed to load config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
function validateConfig(config) {
|
|
4087
|
+
const errors = [];
|
|
4088
|
+
if (!config.name) {
|
|
4089
|
+
errors.push("Missing required field: name");
|
|
4090
|
+
}
|
|
4091
|
+
if (config.computeLayer && !["serverless", "dedicated"].includes(config.computeLayer)) {
|
|
4092
|
+
errors.push(`Invalid computeLayer: ${config.computeLayer}. Must be 'serverless' or 'dedicated'`);
|
|
4093
|
+
}
|
|
4094
|
+
return { valid: errors.length === 0, errors };
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
// src/config/utils.ts
|
|
4098
|
+
function getAllEnvKeys(config) {
|
|
4099
|
+
const provision = config.provision && "env" in config.provision ? config.provision : void 0;
|
|
4100
|
+
const globalKeys = provision?.env ? Object.keys(provision.env) : [];
|
|
4101
|
+
return {
|
|
4102
|
+
global: globalKeys,
|
|
4103
|
+
install: []
|
|
4104
|
+
};
|
|
4105
|
+
}
|
|
4106
|
+
function getRequiredInstallEnvKeys(config) {
|
|
4107
|
+
const provision = config.provision && "env" in config.provision ? config.provision : void 0;
|
|
4108
|
+
if (!provision?.env) return [];
|
|
4109
|
+
return Object.entries(provision.env).filter(([, def]) => def.required).map(([key]) => key);
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
// src/index.ts
|
|
4113
|
+
var src_default = { z: z6 };
|
|
4114
|
+
export {
|
|
4115
|
+
AgentDefinitionSchema,
|
|
4116
|
+
AlertComponentDefinitionSchema,
|
|
4117
|
+
AppAuthInvalidError,
|
|
4118
|
+
AppFieldVisibilitySchema,
|
|
4119
|
+
AuthenticationError,
|
|
4120
|
+
BreadcrumbItemSchema,
|
|
4121
|
+
CONFIG_FILE_NAMES,
|
|
4122
|
+
CardBlockDefinitionSchema,
|
|
4123
|
+
CardBlockHeaderSchema,
|
|
4124
|
+
ChannelCapabilitySchema,
|
|
4125
|
+
ChannelCapabilityTypeSchema,
|
|
4126
|
+
ChannelDefinitionSchema,
|
|
4127
|
+
ChannelDependencySchema,
|
|
4128
|
+
ChannelFieldDefinitionSchema,
|
|
4129
|
+
CheckboxComponentDefinitionSchema,
|
|
4130
|
+
ComboboxComponentDefinitionSchema,
|
|
4131
|
+
ComputeLayerTypeSchema,
|
|
4132
|
+
ConnectionError,
|
|
4133
|
+
DEFAULT_DOCKERFILE,
|
|
4134
|
+
DatePickerComponentDefinitionSchema,
|
|
4135
|
+
EmptyFormComponentDefinitionSchema,
|
|
4136
|
+
EnvSchemaSchema,
|
|
4137
|
+
EnvVariableDefinitionSchema,
|
|
4138
|
+
EnvVisibilitySchema,
|
|
4139
|
+
FieldDataTypeSchema,
|
|
4140
|
+
FieldOptionSchema,
|
|
4141
|
+
FieldOwnerSchema,
|
|
4142
|
+
FieldSettingButtonPropsSchema,
|
|
4143
|
+
FieldSettingComponentDefinitionSchema,
|
|
4144
|
+
FileSettingComponentDefinitionSchema,
|
|
4145
|
+
FilterConditionSchema,
|
|
4146
|
+
FilterOperatorSchema,
|
|
4147
|
+
FormLayoutColumnDefinitionSchema,
|
|
4148
|
+
FormLayoutConfigDefinitionSchema,
|
|
4149
|
+
FormLayoutRowDefinitionSchema,
|
|
4150
|
+
FormV2ComponentDefinitionSchema,
|
|
4151
|
+
FormV2PropsDefinitionSchema,
|
|
4152
|
+
FormV2StylePropsSchema,
|
|
4153
|
+
ImageSettingComponentDefinitionSchema,
|
|
4154
|
+
InlineFieldDefinitionSchema,
|
|
4155
|
+
InputComponentDefinitionSchema,
|
|
4156
|
+
InstallConfigSchema,
|
|
4157
|
+
InstallError,
|
|
4158
|
+
InvalidConfigurationError,
|
|
4159
|
+
LegacyFormBlockDefinitionSchema,
|
|
4160
|
+
ListBlockDefinitionSchema,
|
|
4161
|
+
ListComponentDefinitionSchema,
|
|
4162
|
+
ListItemTemplateSchema,
|
|
4163
|
+
MessageSendChannelSchema,
|
|
4164
|
+
MessageSendContactSchema,
|
|
4165
|
+
MessageSendInputSchema,
|
|
4166
|
+
MessageSendMessageSchema,
|
|
4167
|
+
MessageSendOutputSchema,
|
|
4168
|
+
MessageSendSubscriptionSchema,
|
|
4169
|
+
MissingRequiredFieldError,
|
|
4170
|
+
ModalFormDefinitionSchema,
|
|
4171
|
+
ModelDefinitionSchema,
|
|
4172
|
+
ModelDependencySchema,
|
|
4173
|
+
ModelFieldDefinitionSchema,
|
|
4174
|
+
ModelMapperBlockDefinitionSchema,
|
|
4175
|
+
NavigationBreadcrumbSchema,
|
|
4176
|
+
NavigationConfigSchema,
|
|
4177
|
+
NavigationItemSchema,
|
|
4178
|
+
NavigationSectionSchema,
|
|
4179
|
+
NavigationSidebarSchema,
|
|
4180
|
+
OnDeleteBehaviorSchema,
|
|
4181
|
+
PageActionDefinitionSchema,
|
|
4182
|
+
PageBlockDefinitionSchema,
|
|
4183
|
+
PageBlockTypeSchema,
|
|
4184
|
+
PageContextDefinitionSchema,
|
|
4185
|
+
PageContextFiltersSchema,
|
|
4186
|
+
PageContextItemDefinitionSchema,
|
|
4187
|
+
PageContextModeSchema,
|
|
4188
|
+
PageContextToolItemDefinitionSchema,
|
|
4189
|
+
PageDefinitionSchema,
|
|
4190
|
+
PageFieldDefinitionSchema,
|
|
4191
|
+
PageFieldSourceSchema,
|
|
4192
|
+
PageFieldTypeSchema,
|
|
4193
|
+
PageFormHeaderSchema,
|
|
4194
|
+
PageInstanceFilterSchema,
|
|
4195
|
+
PageTypeSchema,
|
|
4196
|
+
ProvisionConfigSchema,
|
|
4197
|
+
RelationshipCardinalitySchema,
|
|
4198
|
+
RelationshipDefinitionSchema,
|
|
4199
|
+
RelationshipExtensionSchema,
|
|
4200
|
+
RelationshipLinkSchema,
|
|
4201
|
+
ResourceDependencySchema,
|
|
4202
|
+
SelectComponentDefinitionSchema,
|
|
4203
|
+
SkedyulConfigSchema,
|
|
4204
|
+
StructuredFilterSchema,
|
|
4205
|
+
TextareaComponentDefinitionSchema,
|
|
4206
|
+
TimePickerComponentDefinitionSchema,
|
|
4207
|
+
ToolResponseMetaSchema,
|
|
4208
|
+
WebhookHandlerDefinitionSchema,
|
|
4209
|
+
WebhookHttpMethodSchema,
|
|
4210
|
+
WebhookTypeSchema,
|
|
4211
|
+
WebhooksSchema,
|
|
4212
|
+
WorkflowActionInputSchema,
|
|
4213
|
+
WorkflowActionSchema,
|
|
4214
|
+
WorkflowDefinitionSchema,
|
|
4215
|
+
WorkflowDependencySchema,
|
|
4216
|
+
ai,
|
|
4217
|
+
communicationChannel,
|
|
4218
|
+
configure,
|
|
4219
|
+
createContextLogger,
|
|
4220
|
+
createServerHookContext,
|
|
4221
|
+
createToolCallContext,
|
|
4222
|
+
createWebhookContext,
|
|
4223
|
+
createWorkflowStepContext,
|
|
4224
|
+
src_default as default,
|
|
4225
|
+
defineAgent,
|
|
4226
|
+
defineChannel,
|
|
4227
|
+
defineConfig,
|
|
4228
|
+
defineEnv,
|
|
4229
|
+
defineModel,
|
|
4230
|
+
defineNavigation,
|
|
4231
|
+
definePage,
|
|
4232
|
+
defineWorkflow,
|
|
4233
|
+
file,
|
|
4234
|
+
getAllEnvKeys,
|
|
4235
|
+
getConfig,
|
|
4236
|
+
getRequiredInstallEnvKeys,
|
|
4237
|
+
instance,
|
|
4238
|
+
isChannelDependency,
|
|
4239
|
+
isModelDependency,
|
|
4240
|
+
isProvisionContext,
|
|
4241
|
+
isRuntimeContext,
|
|
4242
|
+
isRuntimeWebhookContext,
|
|
4243
|
+
isWorkflowDependency,
|
|
4244
|
+
loadConfig,
|
|
4245
|
+
report,
|
|
4246
|
+
resource,
|
|
4247
|
+
runWithConfig,
|
|
4248
|
+
safeParseConfig,
|
|
4249
|
+
server,
|
|
4250
|
+
token,
|
|
4251
|
+
validateConfig,
|
|
4252
|
+
webhook,
|
|
4253
|
+
workplace,
|
|
4254
|
+
z6 as z
|
|
4255
|
+
};
|