@ganju/utils 0.0.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.
Files changed (220) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +9 -0
  3. package/README.md +23 -0
  4. package/dist/apiError.d.ts +3 -0
  5. package/dist/apiError.d.ts.map +1 -0
  6. package/dist/apiError.js +36 -0
  7. package/dist/attachment.d.ts +33 -0
  8. package/dist/attachment.d.ts.map +1 -0
  9. package/dist/attachment.js +59 -0
  10. package/dist/base64.d.ts +10 -0
  11. package/dist/base64.d.ts.map +1 -0
  12. package/dist/base64.js +50 -0
  13. package/dist/channelDebounce.d.ts +26 -0
  14. package/dist/channelDebounce.d.ts.map +1 -0
  15. package/dist/channelDebounce.js +35 -0
  16. package/dist/channelNotifier.d.ts +9 -0
  17. package/dist/channelNotifier.d.ts.map +1 -0
  18. package/dist/channelNotifier.js +31 -0
  19. package/dist/chunking.d.ts +28 -0
  20. package/dist/chunking.d.ts.map +1 -0
  21. package/dist/chunking.js +97 -0
  22. package/dist/cliConstants.d.ts +9 -0
  23. package/dist/cliConstants.d.ts.map +1 -0
  24. package/dist/cliConstants.js +47 -0
  25. package/dist/coerce.d.ts +2 -0
  26. package/dist/coerce.d.ts.map +1 -0
  27. package/dist/coerce.js +13 -0
  28. package/dist/constants.d.ts +624 -0
  29. package/dist/constants.d.ts.map +1 -0
  30. package/dist/constants.js +2220 -0
  31. package/dist/crypto.d.ts +14 -0
  32. package/dist/crypto.d.ts.map +1 -0
  33. package/dist/crypto.js +93 -0
  34. package/dist/customCodeProject.d.ts +91 -0
  35. package/dist/customCodeProject.d.ts.map +1 -0
  36. package/dist/customCodeProject.js +154 -0
  37. package/dist/customCodeToken.d.ts +41 -0
  38. package/dist/customCodeToken.d.ts.map +1 -0
  39. package/dist/customCodeToken.js +115 -0
  40. package/dist/deadline.d.ts +6 -0
  41. package/dist/deadline.d.ts.map +1 -0
  42. package/dist/deadline.js +45 -0
  43. package/dist/discordSend.d.ts +25 -0
  44. package/dist/discordSend.d.ts.map +1 -0
  45. package/dist/discordSend.js +8 -0
  46. package/dist/embeddable.d.ts +2 -0
  47. package/dist/embeddable.d.ts.map +1 -0
  48. package/dist/embeddable.js +6 -0
  49. package/dist/exposedResource.d.ts +6 -0
  50. package/dist/exposedResource.d.ts.map +1 -0
  51. package/dist/exposedResource.js +18 -0
  52. package/dist/extractedDocument.d.ts +70 -0
  53. package/dist/extractedDocument.d.ts.map +1 -0
  54. package/dist/extractedDocument.js +2 -0
  55. package/dist/fetcher.d.ts +8 -0
  56. package/dist/fetcher.d.ts.map +1 -0
  57. package/dist/fetcher.js +68 -0
  58. package/dist/formatFilename.d.ts +2 -0
  59. package/dist/formatFilename.d.ts.map +1 -0
  60. package/dist/formatFilename.js +19 -0
  61. package/dist/formatRelative.d.ts +12 -0
  62. package/dist/formatRelative.d.ts.map +1 -0
  63. package/dist/formatRelative.js +28 -0
  64. package/dist/formatRelativeTime.d.ts +2 -0
  65. package/dist/formatRelativeTime.d.ts.map +1 -0
  66. package/dist/formatRelativeTime.js +49 -0
  67. package/dist/getEnv.d.ts +5 -0
  68. package/dist/getEnv.d.ts.map +1 -0
  69. package/dist/getEnv.js +11 -0
  70. package/dist/gmailSend.d.ts +24 -0
  71. package/dist/gmailSend.d.ts.map +1 -0
  72. package/dist/gmailSend.js +9 -0
  73. package/dist/index.d.ts +2839 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +155 -0
  76. package/dist/jsonSchemaToZodShape.d.ts +38 -0
  77. package/dist/jsonSchemaToZodShape.d.ts.map +1 -0
  78. package/dist/jsonSchemaToZodShape.js +117 -0
  79. package/dist/l2Normalize.d.ts +2 -0
  80. package/dist/l2Normalize.d.ts.map +1 -0
  81. package/dist/l2Normalize.js +25 -0
  82. package/dist/languageCookieDomain.d.ts +21 -0
  83. package/dist/languageCookieDomain.d.ts.map +1 -0
  84. package/dist/languageCookieDomain.js +39 -0
  85. package/dist/localizeZodIssue.d.ts +43 -0
  86. package/dist/localizeZodIssue.d.ts.map +1 -0
  87. package/dist/localizeZodIssue.js +90 -0
  88. package/dist/mailHeaders.d.ts +4 -0
  89. package/dist/mailHeaders.d.ts.map +1 -0
  90. package/dist/mailHeaders.js +11 -0
  91. package/dist/mcpProxy.d.ts +2 -0
  92. package/dist/mcpProxy.d.ts.map +1 -0
  93. package/dist/mcpProxy.js +21 -0
  94. package/dist/mimeMessage.d.ts +21 -0
  95. package/dist/mimeMessage.d.ts.map +1 -0
  96. package/dist/mimeMessage.js +58 -0
  97. package/dist/oauth.d.ts +21 -0
  98. package/dist/oauth.d.ts.map +1 -0
  99. package/dist/oauth.js +75 -0
  100. package/dist/oauthProviders.d.ts +12 -0
  101. package/dist/oauthProviders.d.ts.map +1 -0
  102. package/dist/oauthProviders.js +99 -0
  103. package/dist/outlookSend.d.ts +19 -0
  104. package/dist/outlookSend.d.ts.map +1 -0
  105. package/dist/outlookSend.js +9 -0
  106. package/dist/parseHttpError.d.ts +2 -0
  107. package/dist/parseHttpError.d.ts.map +1 -0
  108. package/dist/parseHttpError.js +23 -0
  109. package/dist/planLimitError.d.ts +18 -0
  110. package/dist/planLimitError.d.ts.map +1 -0
  111. package/dist/planLimitError.js +35 -0
  112. package/dist/processQueueBatch.d.ts +17 -0
  113. package/dist/processQueueBatch.d.ts.map +1 -0
  114. package/dist/processQueueBatch.js +32 -0
  115. package/dist/reservedToolName.d.ts +16 -0
  116. package/dist/reservedToolName.d.ts.map +1 -0
  117. package/dist/reservedToolName.js +25 -0
  118. package/dist/resourceUri.d.ts +2 -0
  119. package/dist/resourceUri.d.ts.map +1 -0
  120. package/dist/resourceUri.js +19 -0
  121. package/dist/retry.d.ts +10 -0
  122. package/dist/retry.d.ts.map +1 -0
  123. package/dist/retry.js +37 -0
  124. package/dist/sanitize.d.ts +7 -0
  125. package/dist/sanitize.d.ts.map +1 -0
  126. package/dist/sanitize.js +69 -0
  127. package/dist/schema.d.ts +2175 -0
  128. package/dist/schema.d.ts.map +1 -0
  129. package/dist/schema.js +1382 -0
  130. package/dist/sdkConstants.d.ts +15 -0
  131. package/dist/sdkConstants.d.ts.map +1 -0
  132. package/dist/sdkConstants.js +43 -0
  133. package/dist/slackSend.d.ts +38 -0
  134. package/dist/slackSend.d.ts.map +1 -0
  135. package/dist/slackSend.js +14 -0
  136. package/dist/slug.d.ts +5 -0
  137. package/dist/slug.d.ts.map +1 -0
  138. package/dist/slug.js +34 -0
  139. package/dist/slugifyTitle.d.ts +2 -0
  140. package/dist/slugifyTitle.d.ts.map +1 -0
  141. package/dist/slugifyTitle.js +11 -0
  142. package/dist/sources.d.ts +43 -0
  143. package/dist/sources.d.ts.map +1 -0
  144. package/dist/sources.js +73 -0
  145. package/dist/ssrf.d.ts +3 -0
  146. package/dist/ssrf.d.ts.map +1 -0
  147. package/dist/ssrf.js +46 -0
  148. package/dist/tallyUsageKinds.d.ts +9 -0
  149. package/dist/tallyUsageKinds.d.ts.map +1 -0
  150. package/dist/tallyUsageKinds.js +20 -0
  151. package/dist/telegramSend.d.ts +28 -0
  152. package/dist/telegramSend.d.ts.map +1 -0
  153. package/dist/telegramSend.js +9 -0
  154. package/dist/toolCatalog.d.ts +452 -0
  155. package/dist/toolCatalog.d.ts.map +1 -0
  156. package/dist/toolCatalog.js +552 -0
  157. package/dist/validateMessageVariables.d.ts +7 -0
  158. package/dist/validateMessageVariables.d.ts.map +1 -0
  159. package/dist/validateMessageVariables.js +16 -0
  160. package/dist/vendorError.d.ts +2 -0
  161. package/dist/vendorError.d.ts.map +1 -0
  162. package/dist/vendorError.js +25 -0
  163. package/dist/whatsappSend.d.ts +31 -0
  164. package/dist/whatsappSend.d.ts.map +1 -0
  165. package/dist/whatsappSend.js +11 -0
  166. package/package.json +76 -0
  167. package/src/apiError.ts +40 -0
  168. package/src/attachment.ts +97 -0
  169. package/src/base64.ts +60 -0
  170. package/src/channelDebounce.ts +76 -0
  171. package/src/channelNotifier.ts +39 -0
  172. package/src/chunking.ts +131 -0
  173. package/src/cliConstants.ts +50 -0
  174. package/src/coerce.ts +9 -0
  175. package/src/constants.ts +2542 -0
  176. package/src/crypto.ts +108 -0
  177. package/src/customCodeProject.ts +182 -0
  178. package/src/customCodeToken.ts +198 -0
  179. package/src/deadline.ts +43 -0
  180. package/src/discordSend.ts +41 -0
  181. package/src/embeddable.ts +4 -0
  182. package/src/exposedResource.ts +40 -0
  183. package/src/extractedDocument.ts +62 -0
  184. package/src/fetcher.ts +82 -0
  185. package/src/formatFilename.ts +19 -0
  186. package/src/formatRelative.ts +36 -0
  187. package/src/formatRelativeTime.ts +45 -0
  188. package/src/getEnv.ts +13 -0
  189. package/src/gmailSend.ts +34 -0
  190. package/src/index.ts +397 -0
  191. package/src/jsonSchemaToZodShape.ts +111 -0
  192. package/src/l2Normalize.ts +19 -0
  193. package/src/languageCookieDomain.ts +37 -0
  194. package/src/localizeZodIssue.ts +124 -0
  195. package/src/mailHeaders.ts +12 -0
  196. package/src/mcpProxy.ts +20 -0
  197. package/src/mimeMessage.ts +82 -0
  198. package/src/oauth.ts +103 -0
  199. package/src/oauthProviders.ts +118 -0
  200. package/src/outlookSend.ts +44 -0
  201. package/src/parseHttpError.ts +16 -0
  202. package/src/planLimitError.ts +50 -0
  203. package/src/processQueueBatch.ts +46 -0
  204. package/src/reservedToolName.ts +27 -0
  205. package/src/resourceUri.ts +17 -0
  206. package/src/retry.ts +43 -0
  207. package/src/sanitize.ts +67 -0
  208. package/src/schema.ts +1624 -0
  209. package/src/sdkConstants.ts +44 -0
  210. package/src/slackSend.ts +65 -0
  211. package/src/slug.ts +37 -0
  212. package/src/slugifyTitle.ts +8 -0
  213. package/src/sources.ts +117 -0
  214. package/src/ssrf.ts +34 -0
  215. package/src/tallyUsageKinds.ts +20 -0
  216. package/src/telegramSend.ts +39 -0
  217. package/src/toolCatalog.ts +635 -0
  218. package/src/validateMessageVariables.ts +18 -0
  219. package/src/vendorError.ts +19 -0
  220. package/src/whatsappSend.ts +47 -0
package/src/schema.ts ADDED
@@ -0,0 +1,1624 @@
1
+ import { z } from 'zod';
2
+
3
+ import { constants } from './constants';
4
+ import { isReservedSlug, isValidSlugFormat } from './slug';
5
+ import { isReservedToolName } from './reservedToolName';
6
+ import { slugifyTitle } from './slugifyTitle';
7
+
8
+ // A prompt title becomes a slash command; it must not collide with a command
9
+ // the channel runner handles itself (e.g. `/link`).
10
+ const PROMPT_TITLE = z
11
+ .string()
12
+ .min(3)
13
+ .max(200)
14
+ .refine(
15
+ title => !constants.RESERVED_BOT_COMMANDS.includes(slugifyTitle(title)),
16
+ { message: 'This title is reserved as a bot command' }
17
+ );
18
+
19
+ const SCHEMA_DEFINITION = z.object({
20
+ type: z.enum(constants.SCHEMA_DEFINITION_TYPES),
21
+ properties: z.record(z.string(), z.any()).optional(),
22
+ required: z.array(z.string()).optional(),
23
+ items: z.any().optional(),
24
+ minimum: z.number().optional(),
25
+ maximum: z.number().optional(),
26
+ minLength: z.number().optional(),
27
+ maxLength: z.number().optional(),
28
+ pattern: z.string().optional(),
29
+ enum: z.array(z.any()).optional()
30
+ });
31
+
32
+ const ORGANIZATION_CREATE = z.object({
33
+ userId: z.uuid(),
34
+ name: z.string().min(3).max(100),
35
+ projectName: z.string().min(3).max(100),
36
+ projectDescription: z.string().max(500)
37
+ });
38
+
39
+ const ORGANIZATION_CREATE_VIEW = z.object({
40
+ name: z.string().min(3).max(100),
41
+ projectName: z.string().min(3).max(100),
42
+ projectDescription: z.string().max(500)
43
+ });
44
+
45
+ const ORGANIZATION_UPDATE = z.object({
46
+ id: z.uuid(),
47
+ userId: z.uuid(),
48
+ name: z.string().min(3).max(100)
49
+ });
50
+
51
+ const ORGANIZATION_GET = z.object({
52
+ id: z.uuid(),
53
+ userId: z.uuid()
54
+ });
55
+
56
+ const AUTH_USER_GET = z.object({
57
+ userId: z.uuid()
58
+ });
59
+
60
+ const PROJECT_CREATE = z.object({
61
+ userId: z.uuid(),
62
+ organizationId: z.uuid(),
63
+ name: z.string().min(3).max(100),
64
+ description: z.string().max(500).optional()
65
+ });
66
+
67
+ const PROJECT_CREATE_VIEW = PROJECT_CREATE.omit({
68
+ userId: true,
69
+ organizationId: true
70
+ });
71
+
72
+ const PROJECT_UPDATE = z.object({
73
+ id: z.uuid(),
74
+ userId: z.uuid(),
75
+ organizationId: z.uuid(),
76
+ name: z.string().min(3).max(100),
77
+ description: z.string().max(500).optional()
78
+ });
79
+
80
+ const PROJECT_GET = z.object({
81
+ id: z.uuid(),
82
+ organizationId: z.uuid(),
83
+ userId: z.uuid()
84
+ });
85
+
86
+ // Trim + lowercase before validating so invitations match regardless of how
87
+ // the inviter typed the address (membership lookups compare lowercased email).
88
+ const INVITATION_EMAIL = z
89
+ .string()
90
+ .trim()
91
+ .toLowerCase()
92
+ .pipe(z.email({ message: 'Enter a valid email address' }).max(254));
93
+
94
+ // Marketing-site contact form. `company_url` is a honeypot — a hidden field
95
+ // real users leave empty; the controller drops the submission if it's filled.
96
+ const CONTACT_MESSAGE = z.object({
97
+ name: z.string().trim().min(1).max(constants.CONTACT_MAX_NAME_LENGTH),
98
+ email: z
99
+ .string()
100
+ .trim()
101
+ .toLowerCase()
102
+ .pipe(
103
+ z
104
+ .email({ message: 'Enter a valid email address' })
105
+ .max(constants.CONTACT_MAX_EMAIL_LENGTH)
106
+ ),
107
+ message: z
108
+ .string()
109
+ .trim()
110
+ .min(constants.CONTACT_MIN_MESSAGE_LENGTH)
111
+ .max(constants.CONTACT_MAX_MESSAGE_LENGTH),
112
+ company_url: z.string().optional()
113
+ });
114
+
115
+ const ORGANIZATION_INVITATION_CREATE = z.object({
116
+ email: INVITATION_EMAIL,
117
+ userId: z.uuid(),
118
+ organizationId: z.uuid()
119
+ });
120
+
121
+ const ORGANIZATION_INVITATION_CREATE_VIEW = ORGANIZATION_INVITATION_CREATE.omit(
122
+ {
123
+ userId: true,
124
+ organizationId: true
125
+ }
126
+ );
127
+
128
+ const ORGANIZATION_INVITATION_LIST = z.object({
129
+ userId: z.uuid(),
130
+ organizationId: z.uuid()
131
+ });
132
+
133
+ const ORGANIZATION_INVITATION_REMOVE = z.object({
134
+ invitationId: z.uuid(),
135
+ userId: z.uuid(),
136
+ organizationId: z.uuid()
137
+ });
138
+
139
+ const PROJECT_INVITATION_CREATE = z.object({
140
+ email: INVITATION_EMAIL,
141
+ userId: z.uuid(),
142
+ organizationId: z.uuid(),
143
+ projectId: z.uuid()
144
+ });
145
+
146
+ const PROJECT_INVITATION_CREATE_VIEW = PROJECT_INVITATION_CREATE.omit({
147
+ userId: true,
148
+ organizationId: true,
149
+ projectId: true
150
+ });
151
+
152
+ const PROJECT_INVITATION_LIST = z.object({
153
+ userId: z.uuid(),
154
+ organizationId: z.uuid(),
155
+ projectId: z.uuid()
156
+ });
157
+
158
+ const PROJECT_INVITATION_REMOVE = z.object({
159
+ invitationId: z.uuid(),
160
+ userId: z.uuid(),
161
+ organizationId: z.uuid(),
162
+ projectId: z.uuid()
163
+ });
164
+
165
+ // Invitee acting on one of their own pending invitations. No org/project id —
166
+ // the invitation is matched to the caller by id + session email.
167
+ const INVITATION_RESPOND = z.object({
168
+ invitationId: z.uuid(),
169
+ userId: z.uuid(),
170
+ action: z.enum(constants.INVITATION_RESPONSES)
171
+ });
172
+
173
+ const INVITATION_GET_BY_TOKEN = z.object({
174
+ token: z.string().min(8).max(128)
175
+ });
176
+
177
+ const ORGANIZATION_MEMBER_LIST = z.object({
178
+ userId: z.uuid(),
179
+ organizationId: z.uuid()
180
+ });
181
+
182
+ const ORGANIZATION_MEMBER_REMOVE = z.object({
183
+ memberUserId: z.uuid(),
184
+ userId: z.uuid(),
185
+ organizationId: z.uuid()
186
+ });
187
+
188
+ const PROJECT_MEMBER_LIST = z.object({
189
+ userId: z.uuid(),
190
+ organizationId: z.uuid(),
191
+ projectId: z.uuid()
192
+ });
193
+
194
+ const PROJECT_MEMBER_REMOVE = z.object({
195
+ memberUserId: z.uuid(),
196
+ userId: z.uuid(),
197
+ organizationId: z.uuid(),
198
+ projectId: z.uuid()
199
+ });
200
+
201
+ const ARTIFACT_CREATE_PROMPT = z.object({
202
+ title: PROMPT_TITLE,
203
+ description: z.string().max(1000).optional(),
204
+ messages: z
205
+ .array(
206
+ z.object({
207
+ role: z.enum(constants.ROLE_MESSAGES),
208
+ content: z.string()
209
+ })
210
+ )
211
+ .min(1),
212
+ schema: SCHEMA_DEFINITION,
213
+ projectId: z.uuid(),
214
+ userId: z.uuid(),
215
+ organizationId: z.uuid()
216
+ });
217
+
218
+ const ARTIFACT_UPDATE_PROMPT = z.object({
219
+ promptId: z.uuid(),
220
+ title: PROMPT_TITLE,
221
+ description: z.string().max(1000).optional(),
222
+ messages: z
223
+ .array(
224
+ z.object({
225
+ role: z.enum(constants.ROLE_MESSAGES),
226
+ content: z.string()
227
+ })
228
+ )
229
+ .min(1),
230
+ schema: SCHEMA_DEFINITION,
231
+ projectId: z.uuid(),
232
+ userId: z.uuid(),
233
+ organizationId: z.uuid()
234
+ });
235
+
236
+ const ARTIFACT_REMOVE_PROMPT = z.object({
237
+ promptId: z.uuid(),
238
+ projectId: z.uuid(),
239
+ userId: z.uuid(),
240
+ organizationId: z.uuid()
241
+ });
242
+
243
+ const ARTIFACT_GET_PROMPT = z.object({
244
+ projectId: z.uuid(),
245
+ userId: z.uuid(),
246
+ organizationId: z.uuid()
247
+ });
248
+
249
+ const CRAWL_CONFIG = z.object({
250
+ maxPages: z
251
+ .number()
252
+ .int()
253
+ .min(1)
254
+ .max(constants.CRAWL_MAX_PAGES_LIMIT)
255
+ .default(constants.CRAWL_DEFAULT_MAX_PAGES),
256
+ maxDepth: z
257
+ .number()
258
+ .int()
259
+ .min(0)
260
+ .max(constants.CRAWL_MAX_DEPTH_LIMIT)
261
+ .default(constants.CRAWL_DEFAULT_MAX_DEPTH)
262
+ });
263
+
264
+ const ARTIFACT_CREATE_WEBSITE_VIEW = z.object({
265
+ title: z.string().min(3).max(200),
266
+ uri: z
267
+ .url('Enter a valid URL')
268
+ .refine(
269
+ v => /^https?:\/\//i.test(v),
270
+ 'Only http and https URLs are supported'
271
+ ),
272
+ description: z.string().optional(),
273
+ maxPages: z.number().int().min(1).max(constants.CRAWL_MAX_PAGES_LIMIT),
274
+ maxDepth: z.number().int().min(0).max(constants.CRAWL_MAX_DEPTH_LIMIT)
275
+ });
276
+
277
+ const ARTIFACT_CREATE_WEBSITE = z.object({
278
+ title: z.string().min(3).max(200),
279
+ uri: z
280
+ .url('Enter a valid URL')
281
+ .refine(
282
+ v => /^https?:\/\//i.test(v),
283
+ 'Only http and https URLs are supported'
284
+ ),
285
+ description: z.string().optional(),
286
+ crawlConfig: z.object({
287
+ maxPages: z.number().int().min(1).max(constants.CRAWL_MAX_PAGES_LIMIT),
288
+ maxDepth: z.number().int().min(0).max(constants.CRAWL_MAX_DEPTH_LIMIT)
289
+ }),
290
+ projectId: z.uuid(),
291
+ userId: z.uuid(),
292
+ organizationId: z.uuid()
293
+ });
294
+
295
+ const ARTIFACT_UPDATE_WEBSITE_VIEW = z.object({
296
+ title: z.string().min(3).max(200),
297
+ description: z.string().optional()
298
+ });
299
+
300
+ const ARTIFACT_UPDATE_WEBSITE = ARTIFACT_UPDATE_WEBSITE_VIEW.extend({
301
+ resourceId: z.uuid(),
302
+ projectId: z.uuid(),
303
+ userId: z.uuid(),
304
+ organizationId: z.uuid()
305
+ });
306
+
307
+ const GOOGLE_DRIVE_ITEM = z.object({
308
+ fileId: z.string(),
309
+ name: z.string(),
310
+ mimeType: z.string(),
311
+ isFolder: z.boolean().default(false),
312
+ iconLink: z.string().optional(),
313
+ webViewLink: z.string().optional(),
314
+ modifiedTime: z.string().optional(),
315
+ size: z.coerce.number().int().optional()
316
+ });
317
+
318
+ const ARTIFACT_CREATE_GOOGLE_DRIVE_VIEW = z.object({
319
+ items: z.array(GOOGLE_DRIVE_ITEM)
320
+ });
321
+
322
+ const ARTIFACT_CREATE_GOOGLE_DRIVE = ARTIFACT_CREATE_GOOGLE_DRIVE_VIEW.extend({
323
+ projectId: z.uuid(),
324
+ userId: z.uuid(),
325
+ organizationId: z.uuid()
326
+ });
327
+
328
+ const ARTIFACT_SYNC_GOOGLE_DRIVE = z.object({
329
+ resourceId: z.uuid(),
330
+ projectId: z.uuid(),
331
+ userId: z.uuid(),
332
+ organizationId: z.uuid()
333
+ });
334
+
335
+ const ONE_DRIVE_ITEM = z.object({
336
+ itemId: z.string(),
337
+ driveId: z.string().optional(),
338
+ name: z.string(),
339
+ mimeType: z.string().optional(),
340
+ isFolder: z.boolean().default(false),
341
+ webUrl: z.string().optional(),
342
+ lastModifiedDateTime: z.string().optional(),
343
+ size: z.coerce.number().int().optional()
344
+ });
345
+
346
+ const ARTIFACT_CREATE_ONE_DRIVE_VIEW = z.object({
347
+ items: z.array(ONE_DRIVE_ITEM)
348
+ });
349
+
350
+ const ARTIFACT_CREATE_ONE_DRIVE = ARTIFACT_CREATE_ONE_DRIVE_VIEW.extend({
351
+ projectId: z.uuid(),
352
+ userId: z.uuid(),
353
+ organizationId: z.uuid()
354
+ });
355
+
356
+ const ARTIFACT_SYNC_ONE_DRIVE = z.object({
357
+ resourceId: z.uuid(),
358
+ projectId: z.uuid(),
359
+ userId: z.uuid(),
360
+ organizationId: z.uuid()
361
+ });
362
+
363
+ const ARTIFACT_CREATE_RESOURCE = z.object({
364
+ title: z.string().min(3).max(200),
365
+ uri: z.string(),
366
+ type: z
367
+ .enum(constants.RESOURCE_TYPES)
368
+ .default(constants.RESOURCE_TYPE_STATIC),
369
+ sourceType: z
370
+ .enum(constants.RESOURCE_SOURCE_TYPES)
371
+ .default(constants.RESOURCE_SOURCE_TYPE_FILE),
372
+ description: z.string().max(1000).optional(),
373
+ mimeType: z.enum(constants.MIMETYPES, {
374
+ message: 'Unsupported mime type'
375
+ }),
376
+ content: z.string().optional(),
377
+ size: z
378
+ .number()
379
+ .int()
380
+ .min(0)
381
+ .max(constants.MAX_FILE_SIZE, {
382
+ message: `File size exceeds the ${constants.MAX_FILE_SIZE / (1024 * 1024)}MB limit`
383
+ })
384
+ .optional(),
385
+ encoding: z.string().max(50).optional(),
386
+ fileKey: z.string().optional(),
387
+ fileName: z.string().optional(),
388
+ annotations: z
389
+ .object({
390
+ audience: z.array(z.enum(constants.ROLE_MESSAGES)).optional(),
391
+ priority: z.number().min(0).max(1).optional(),
392
+ lastModified: z.string().datetime().optional()
393
+ })
394
+ .optional(),
395
+ icons: z
396
+ .array(
397
+ z.object({
398
+ src: z.string(),
399
+ mimeType: z.string().optional(),
400
+ sizes: z.array(z.string()).optional(),
401
+ theme: z.enum(constants.RESOURCE_ICON_THEMES).optional()
402
+ })
403
+ )
404
+ .optional(),
405
+ metadata: z.record(z.string(), z.any()).optional(),
406
+ crawlConfig: CRAWL_CONFIG.optional(),
407
+ projectId: z.uuid(),
408
+ userId: z.uuid(),
409
+ organizationId: z.uuid()
410
+ });
411
+
412
+ const ARTIFACT_UPDATE_RESOURCE = z.object({
413
+ resourceId: z.uuid(),
414
+ title: z.string().min(3).max(200),
415
+ uri: z.string(),
416
+ type: z
417
+ .enum(constants.RESOURCE_TYPES)
418
+ .default(constants.RESOURCE_TYPE_STATIC),
419
+ sourceType: z
420
+ .enum(constants.RESOURCE_SOURCE_TYPES)
421
+ .default(constants.RESOURCE_SOURCE_TYPE_FILE),
422
+ description: z.string().max(1000).optional(),
423
+ mimeType: z.enum(constants.MIMETYPES, {
424
+ message: 'Unsupported mime type'
425
+ }),
426
+ content: z.string().optional(),
427
+ size: z
428
+ .number()
429
+ .int()
430
+ .min(0)
431
+ .max(constants.MAX_FILE_SIZE, {
432
+ message: `File size exceeds the ${constants.MAX_FILE_SIZE / (1024 * 1024)}MB limit`
433
+ })
434
+ .optional(),
435
+ encoding: z.string().max(50).optional(),
436
+ fileKey: z.string().optional(),
437
+ fileName: z.string().optional(),
438
+ annotations: z
439
+ .object({
440
+ audience: z.array(z.enum(constants.ROLE_MESSAGES)).optional(),
441
+ priority: z.number().min(0).max(1).optional(),
442
+ lastModified: z.string().datetime().optional()
443
+ })
444
+ .optional(),
445
+ icons: z
446
+ .array(
447
+ z.object({
448
+ src: z.string(),
449
+ mimeType: z.string().optional(),
450
+ sizes: z.array(z.string()).optional(),
451
+ theme: z.enum(constants.RESOURCE_ICON_THEMES).optional()
452
+ })
453
+ )
454
+ .optional(),
455
+ metadata: z.record(z.string(), z.any()).optional(),
456
+ projectId: z.uuid(),
457
+ userId: z.uuid(),
458
+ organizationId: z.uuid()
459
+ });
460
+
461
+ const ARTIFACT_GET_RESOURCE = z.object({
462
+ projectId: z.uuid(),
463
+ userId: z.uuid(),
464
+ organizationId: z.uuid()
465
+ });
466
+
467
+ const ARTIFACT_GET_RESOURCE_BY_ID = z.object({
468
+ resourceId: z.uuid(),
469
+ projectId: z.uuid(),
470
+ userId: z.uuid(),
471
+ organizationId: z.uuid()
472
+ });
473
+
474
+ const ARTIFACT_REMOVE_RESOURCE = z.object({
475
+ resourceId: z.uuid(),
476
+ projectId: z.uuid(),
477
+ userId: z.uuid(),
478
+ organizationId: z.uuid()
479
+ });
480
+
481
+ const ARTIFACT_UPLOAD_RESOURCE_FILE = z.object({
482
+ resourceId: z.uuid(),
483
+ projectId: z.uuid(),
484
+ userId: z.uuid(),
485
+ organizationId: z.uuid()
486
+ });
487
+
488
+ const ARTIFACT_DOWNLOAD_RESOURCE_FILE = z.object({
489
+ resourceId: z.uuid(),
490
+ projectId: z.uuid(),
491
+ userId: z.uuid(),
492
+ organizationId: z.uuid()
493
+ });
494
+
495
+ const ARTIFACT_CREATE_TOOL = z.object({
496
+ toolKey: z.string().min(1).max(64),
497
+ config: z.record(z.string(), z.any()).optional(),
498
+ metadata: z.record(z.string(), z.any()).optional(),
499
+ projectId: z.uuid(),
500
+ userId: z.uuid(),
501
+ organizationId: z.uuid()
502
+ });
503
+
504
+ const ARTIFACT_UPDATE_TOOL = z.object({
505
+ toolId: z.uuid(),
506
+ config: z.record(z.string(), z.any()).optional(),
507
+ metadata: z.record(z.string(), z.any()).optional(),
508
+ projectId: z.uuid(),
509
+ userId: z.uuid(),
510
+ organizationId: z.uuid()
511
+ });
512
+
513
+ const ARTIFACT_GET_TOOL = z.object({
514
+ projectId: z.uuid(),
515
+ userId: z.uuid(),
516
+ organizationId: z.uuid()
517
+ });
518
+
519
+ const ARTIFACT_REMOVE_TOOL = z.object({
520
+ toolId: z.uuid(),
521
+ projectId: z.uuid(),
522
+ userId: z.uuid(),
523
+ organizationId: z.uuid()
524
+ });
525
+
526
+ // Turning one tool on or off. Deliberately its own route rather than a field on
527
+ // ARTIFACT_UPDATE_TOOL: that path re-validates config and, for mcp-proxy,
528
+ // re-runs remote discovery — a network round trip nobody should pay to flip a
529
+ // switch, and one that would fail the toggle whenever the remote is down.
530
+ const ARTIFACT_SET_TOOL_ENABLED = z.object({
531
+ toolId: z.uuid(),
532
+ enabled: z.boolean(),
533
+ projectId: z.uuid(),
534
+ userId: z.uuid(),
535
+ organizationId: z.uuid()
536
+ });
537
+
538
+ const ARTIFACT_GET_CREDENTIAL = z.object({
539
+ projectId: z.uuid(),
540
+ userId: z.uuid(),
541
+ organizationId: z.uuid()
542
+ });
543
+
544
+ const ARTIFACT_REMOVE_CREDENTIAL = z.object({
545
+ credentialId: z.uuid(),
546
+ projectId: z.uuid(),
547
+ userId: z.uuid(),
548
+ organizationId: z.uuid()
549
+ });
550
+
551
+ // Reads every managed OAuth provider and where the artifact stands with it.
552
+ // Same fields as the credential read above, kept separate because the two answer
553
+ // different questions — one lists stored secrets, the other lists connections
554
+ // whether or not one exists yet.
555
+ const ARTIFACT_LIST_CONNECTIONS = z.object({
556
+ projectId: z.uuid(),
557
+ userId: z.uuid(),
558
+ organizationId: z.uuid()
559
+ });
560
+
561
+ const ARTIFACT_CREATE_CREDENTIAL = z.object({
562
+ provider: z.enum(constants.CREDENTIAL_PROVIDERS),
563
+ apiKey: z.string().min(1),
564
+ label: z.string().trim().min(1).max(100).optional(),
565
+ projectId: z.uuid(),
566
+ userId: z.uuid(),
567
+ organizationId: z.uuid()
568
+ });
569
+
570
+ const ORGANIZATION_LIST_LLM = z.object({
571
+ userId: z.uuid(),
572
+ organizationId: z.uuid()
573
+ });
574
+
575
+ const ORGANIZATION_CREATE_LLM = z.object({
576
+ name: z.string().min(1).max(200),
577
+ provider: z.enum(constants.LLM_PROVIDERS),
578
+ model: z.string().min(1).max(200),
579
+ baseUrl: z.url().optional().or(z.literal('')),
580
+ apiKey: z.string().min(1).max(500),
581
+ systemPrompt: z.string().max(10000).optional(),
582
+ config: z.record(z.string(), z.any()).optional(),
583
+ userId: z.uuid(),
584
+ organizationId: z.uuid()
585
+ });
586
+
587
+ const ORGANIZATION_UPDATE_LLM = z.object({
588
+ llmId: z.uuid(),
589
+ name: z.string().min(1).max(200).optional(),
590
+ provider: z.enum(constants.LLM_PROVIDERS).optional(),
591
+ model: z.string().min(1).max(200).optional(),
592
+ baseUrl: z.url().optional().or(z.literal('')).nullable(),
593
+ apiKey: z.string().min(1).max(500).optional(),
594
+ systemPrompt: z.string().max(10000).optional().nullable(),
595
+ config: z.record(z.string(), z.any()).optional().nullable(),
596
+ userId: z.uuid(),
597
+ organizationId: z.uuid()
598
+ });
599
+
600
+ const ORGANIZATION_REMOVE_LLM = z.object({
601
+ llmId: z.uuid(),
602
+ userId: z.uuid(),
603
+ organizationId: z.uuid()
604
+ });
605
+
606
+ const ORGANIZATION_CREATE_LLM_VIEW = ORGANIZATION_CREATE_LLM.omit({
607
+ userId: true,
608
+ organizationId: true
609
+ });
610
+
611
+ const ORGANIZATION_UPDATE_LLM_VIEW = ORGANIZATION_UPDATE_LLM.omit({
612
+ llmId: true,
613
+ userId: true,
614
+ organizationId: true
615
+ });
616
+
617
+ // `channel.config` is an open bag, so this stays loose — it only pins down the
618
+ // keys we actually read, and passes everything else through untouched.
619
+ // `debounceMs` is how long a burst from one participant is buffered before the
620
+ // agent answers it as a single turn; 0 disables buffering.
621
+ const CHANNEL_CONFIG = z.looseObject({
622
+ debounceMs: z
623
+ .number()
624
+ .int()
625
+ .refine(
626
+ value =>
627
+ value === constants.CHANNEL_DEBOUNCE_DISABLED ||
628
+ (value >= constants.CHANNEL_DEBOUNCE_MIN_MS &&
629
+ value <= constants.CHANNEL_DEBOUNCE_MAX_MS),
630
+ `debounceMs must be 0 (disabled) or between ${constants.CHANNEL_DEBOUNCE_MIN_MS} and ${constants.CHANNEL_DEBOUNCE_MAX_MS} ms`
631
+ )
632
+ .optional()
633
+ });
634
+
635
+ const CHANNEL_CREATE = z.object({
636
+ platform: z.enum(constants.CHANNEL_PLATFORMS),
637
+ config: CHANNEL_CONFIG.optional(),
638
+ credentials: z.record(z.string(), z.string()),
639
+ llmId: z.uuid().nullable().optional(),
640
+ projectId: z.uuid(),
641
+ userId: z.uuid(),
642
+ organizationId: z.uuid()
643
+ });
644
+
645
+ const CHANNEL_UPDATE = z.object({
646
+ channelId: z.uuid(),
647
+ status: z.enum(constants.CHANNEL_STATUS).optional(),
648
+ config: CHANNEL_CONFIG.optional(),
649
+ credentials: z.record(z.string(), z.string()).optional(),
650
+ llmId: z.uuid().nullable().optional(),
651
+ projectId: z.uuid(),
652
+ userId: z.uuid(),
653
+ organizationId: z.uuid()
654
+ });
655
+
656
+ // Validates one artifact_tool.config of definition `http-endpoint`. Each row of
657
+ // this kind registers one named MCP tool at server boot (see apps/mcp tools
658
+ // README). Defaults are filled here so the dispatcher receives a fully-resolved
659
+ // config. Secrets are never inlined — auth references an artifact_credential by
660
+ // id (credentialId), validated as a uuid.
661
+ const HTTP_ENDPOINT_KEY_VALUE = z.object({
662
+ name: z.string().min(1).max(256),
663
+ value: z.string().max(8192)
664
+ });
665
+
666
+ const HTTP_ENDPOINT_AUTH = z.discriminatedUnion('kind', [
667
+ z.object({ kind: z.literal(constants.HTTP_ENDPOINT_AUTH_KIND_NONE) }),
668
+ z.object({
669
+ kind: z.literal(constants.HTTP_ENDPOINT_AUTH_KIND_BEARER),
670
+ credentialId: z.uuid()
671
+ }),
672
+ z.object({
673
+ kind: z.literal(constants.HTTP_ENDPOINT_AUTH_KIND_BASIC),
674
+ credentialId: z.uuid()
675
+ }),
676
+ z.object({
677
+ kind: z.literal(constants.HTTP_ENDPOINT_AUTH_KIND_OAUTH),
678
+ credentialId: z.uuid()
679
+ }),
680
+ z.object({
681
+ kind: z.literal(constants.HTTP_ENDPOINT_AUTH_KIND_API_KEY),
682
+ in: z.enum(['header', 'query']),
683
+ name: z.string().min(1).max(256),
684
+ credentialId: z.uuid()
685
+ })
686
+ ]);
687
+
688
+ const HTTP_ENDPOINT_CONFIG = z
689
+ .object({
690
+ // Identity — surfaced to the model. `name` becomes the MCP tool key.
691
+ name: z
692
+ .string()
693
+ .regex(
694
+ /^[a-zA-Z0-9_-]{1,64}$/,
695
+ 'Tool name must be 1-64 chars: letters, digits, underscore or hyphen'
696
+ ),
697
+ title: z.string().min(1).max(200).optional(),
698
+ description: z.string().max(2000).optional(),
699
+ // Request.
700
+ method: z.preprocess(
701
+ v => (typeof v === 'string' ? v.toUpperCase() : v),
702
+ z.enum(constants.HTTP_ENDPOINT_METHODS)
703
+ ),
704
+ url: z.string().min(1).max(2048),
705
+ headers: z.array(HTTP_ENDPOINT_KEY_VALUE).max(50).default([]),
706
+ query: z.array(HTTP_ENDPOINT_KEY_VALUE).max(50).default([]),
707
+ body: z
708
+ .object({
709
+ kind: z.enum(constants.HTTP_ENDPOINT_BODY_KINDS),
710
+ template: z.string().max(100_000).default('')
711
+ })
712
+ .default({ kind: constants.HTTP_ENDPOINT_BODY_KIND_NONE, template: '' }),
713
+ // Input schema the model fills in (reuses the shared JSON-schema shape).
714
+ inputSchema: SCHEMA_DEFINITION.default({
715
+ type: 'object',
716
+ properties: {}
717
+ }),
718
+ // What the tool promises to return, and the last asymmetry between the two
719
+ // user-authored tool shapes — `custom-code` has carried one since the
720
+ // manifest existed.
721
+ //
722
+ // Optional, and absent on every endpoint that predates it: declaring one
723
+ // turns the response into MCP `structuredContent`, and a client that asked
724
+ // for structure gets an object instead of a wall of text. Leaving it out
725
+ // keeps the text behaviour exactly as it was.
726
+ outputSchema: SCHEMA_DEFINITION.optional(),
727
+ // Response handling.
728
+ response: z
729
+ .object({
730
+ contentType: z
731
+ .enum([
732
+ constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_AUTO,
733
+ constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_JSON,
734
+ constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_TEXT
735
+ ])
736
+ .default(constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_AUTO),
737
+ // Clamp to the hard cap rather than reject an over-large request.
738
+ maxBytes: z
739
+ .number()
740
+ .int()
741
+ .positive()
742
+ .default(constants.HTTP_ENDPOINT_DEFAULT_MAX_BYTES)
743
+ .transform(n =>
744
+ Math.min(n, constants.HTTP_ENDPOINT_DEFAULT_MAX_BYTES)
745
+ ),
746
+ jsonPath: z.string().max(256).optional(),
747
+ successStatus: z
748
+ .array(z.number().int().min(100).max(599))
749
+ .max(20)
750
+ .optional()
751
+ })
752
+ .default({
753
+ contentType: constants.HTTP_ENDPOINT_RESPONSE_CONTENT_TYPE_AUTO,
754
+ maxBytes: constants.HTTP_ENDPOINT_DEFAULT_MAX_BYTES
755
+ }),
756
+ // Auth — credentials referenced by id, never inlined.
757
+ auth: HTTP_ENDPOINT_AUTH.default({
758
+ kind: constants.HTTP_ENDPOINT_AUTH_KIND_NONE
759
+ }),
760
+ // Safety.
761
+ timeoutMs: z
762
+ .number()
763
+ .int()
764
+ .positive()
765
+ .default(constants.HTTP_ENDPOINT_DEFAULT_TIMEOUT_MS)
766
+ .transform(n => Math.min(n, constants.HTTP_ENDPOINT_MAX_TIMEOUT_MS)),
767
+ allowedHosts: z
768
+ .array(z.string().min(1).max(253).toLowerCase())
769
+ .max(50)
770
+ .optional()
771
+ })
772
+ .transform(cfg => ({
773
+ ...cfg,
774
+ title: cfg.title || cfg.name,
775
+ description:
776
+ cfg.description ||
777
+ `Call the configured ${cfg.method} ${cfg.url} endpoint.`
778
+ }));
779
+
780
+ // The same config, plus the rule that its `name` must not be one the platform
781
+ // owns — `name` becomes the MCP tool key, in the same flat namespace the native
782
+ // tools register into. Split from the schema above rather than folded into it
783
+ // because that one is also how apps/mcp READS a stored row at boot: an install
784
+ // that predates this rule, or predates a group being added to it, has to keep
785
+ // registering. It loses the name to the native tool (apps/mcp registers those
786
+ // first) instead of vanishing.
787
+ //
788
+ // Every path that accepts a config from a user validates with this one.
789
+ const HTTP_ENDPOINT_CONFIG_WRITE = HTTP_ENDPOINT_CONFIG.superRefine(
790
+ (cfg, ctx) => {
791
+ if (isReservedToolName(cfg.name)) {
792
+ ctx.addIssue({
793
+ code: 'custom',
794
+ path: ['name'],
795
+ message: constants.RESERVED_TOOL_NAME_MESSAGE
796
+ });
797
+ }
798
+ // On the write path only, like the rule above: no stored row can have one
799
+ // yet, but the read shape must stay permissive on principle — a rule
800
+ // tightened later must never stop an installed tool from registering.
801
+ if (cfg.outputSchema && cfg.outputSchema.type !== 'object') {
802
+ ctx.addIssue({
803
+ code: 'custom',
804
+ path: ['outputSchema', 'type'],
805
+ message: constants.OUTPUT_SCHEMA_NOT_OBJECT_MESSAGE
806
+ });
807
+ }
808
+ }
809
+ );
810
+
811
+ // Validates one artifact_tool.config of definition `mcp-proxy`. Each row of this
812
+ // kind connects a remote MCP server (a vendor's official server) and registers
813
+ // one local MCP tool per discovered remote tool at boot. `url`/`transport` are
814
+ // resolved server-side from the curated mcp_server_catalog row referenced by
815
+ // `curatedServerId` (the client never picks an arbitrary URL for now), then
816
+ // re-validated here. Secrets are never inlined — auth references an
817
+ // artifact_credential by id, same as http-endpoint.
818
+ const MCP_PROXY_AUTH = z.discriminatedUnion('kind', [
819
+ z.object({ kind: z.literal(constants.MCP_PROXY_AUTH_KIND_NONE) }),
820
+ z.object({
821
+ kind: z.literal(constants.MCP_PROXY_AUTH_KIND_BEARER),
822
+ credentialId: z.uuid()
823
+ }),
824
+ z.object({
825
+ kind: z.literal(constants.MCP_PROXY_AUTH_KIND_HEADER),
826
+ name: z.string().min(1).max(256),
827
+ credentialId: z.uuid()
828
+ }),
829
+ z.object({
830
+ kind: z.literal(constants.MCP_PROXY_AUTH_KIND_OAUTH),
831
+ credentialId: z.uuid()
832
+ })
833
+ ]);
834
+
835
+ const MCP_PROXY_CONFIG = z.object({
836
+ // Which curated server this row connects. url/transport are filled from the
837
+ // catalog server-side; included here so the stored config is self-contained
838
+ // for the boot loop (which never re-reads the catalog).
839
+ curatedServerId: z.uuid(),
840
+ url: z.string().min(1).max(2048).default(''),
841
+ transport: z
842
+ .enum(constants.MCP_PROXY_TRANSPORTS)
843
+ .default(constants.MCP_PROXY_TRANSPORT_STREAMABLE_HTTP),
844
+ // Tools register as `<prefix>__<remoteKey>`. Optional — the API defaults it to
845
+ // the catalog slug when unset.
846
+ prefix: z
847
+ .string()
848
+ .regex(
849
+ /^[a-zA-Z0-9_-]{1,40}$/,
850
+ 'Prefix must be 1-40 chars: letters, digits, underscore or hyphen'
851
+ )
852
+ .optional(),
853
+ // Per-item enable lists; the full available set lives on metadata.discovery so
854
+ // the UI can render toggles without re-hitting the remote. The empty/absent
855
+ // semantics differ by kind, matching the UI defaults:
856
+ // - allowedTools: absent/empty = ALL tools enabled (UI defaults them on).
857
+ // - allowedResources / allowedPrompts: opt-in — ONLY the listed items
858
+ // register; absent/empty = none (UI defaults them off).
859
+ // All three are filtered at boot.
860
+ allowedTools: z.array(z.string().min(1).max(256)).max(500).optional(),
861
+ allowedResources: z.array(z.string().min(1).max(2048)).max(500).optional(),
862
+ allowedPrompts: z.array(z.string().min(1).max(256)).max(500).optional(),
863
+ auth: MCP_PROXY_AUTH.default({ kind: constants.MCP_PROXY_AUTH_KIND_NONE }),
864
+ timeoutMs: z
865
+ .number()
866
+ .int()
867
+ .positive()
868
+ .default(constants.MCP_PROXY_DEFAULT_TIMEOUT_MS)
869
+ .transform(n => Math.min(n, constants.MCP_PROXY_MAX_TIMEOUT_MS))
870
+ });
871
+
872
+ // Validates one artifact_tool.config of definition `custom-code`. Unlike the two
873
+ // definitions above, this config describes almost nothing about the tools — the
874
+ // names and schemas live on the ACTIVE VERSION (artifact_tool_version.tools),
875
+ // written at publish time. Everything here is the row-level envelope: which
876
+ // version is live, and the limits that apply to whatever code that version runs.
877
+ //
878
+ // `activeVersionId` is nullable on purpose: a freshly installed row has an
879
+ // uploaded draft but nothing published, and the MCP boot loop must treat that as
880
+ // "this artifact contributes zero tools" rather than as an error.
881
+ const CUSTOM_CODE_CONFIG = z.object({
882
+ activeVersionId: z.uuid().nullable().default(null),
883
+ // Which of the active version's tools are exposed. Absent or empty means all
884
+ // of them — the same convention `mcp-proxy` uses for its remote tools, and
885
+ // reused rather than reinvented so one row of many tools behaves the same way
886
+ // whichever kind of row it is.
887
+ //
888
+ // It lives on the row's config and not in the version, because it answers a
889
+ // different question. The manifest is what this code CAN do and moves only by
890
+ // deploying; this is what the server currently offers, and turning one tool
891
+ // off to shorten a bloated tool list must not require a redeploy or leave the
892
+ // author's manifest disagreeing with their own file.
893
+ //
894
+ // Names that no longer appear in the active manifest are simply never matched,
895
+ // so a version that drops a tool leaves nothing to clean up here.
896
+ allowedTools: z
897
+ .array(z.string().min(1).max(constants.CUSTOM_CODE_TOOL_NAME_MAX))
898
+ .max(constants.CUSTOM_CODE_MAX_TOOLS)
899
+ .optional(),
900
+ // Egress allow-list, enforced by the outbound worker (Phase 2) — never in the
901
+ // SDK, which is user-editable and therefore not a control.
902
+ allowedHosts: z
903
+ .array(z.string().min(1).max(253).toLowerCase())
904
+ .max(50)
905
+ .optional(),
906
+ // Providers the script may request via ctx.connection(), and — for the three
907
+ // that have one — send files as. The broker refuses anything not listed here,
908
+ // so widening it is an explicit, auditable edit.
909
+ //
910
+ // Left as a free string on this READ shape: the connectable set grows and
911
+ // shrinks as providers are added and retired, and a stored row naming a
912
+ // provider we no longer offer has to keep parsing or the whole install stops
913
+ // registering. CUSTOM_CODE_CONFIG_WRITE below is where a name is checked.
914
+ connections: z.array(z.string().min(1).max(100)).max(50).optional(),
915
+ timeoutMs: z
916
+ .number()
917
+ .int()
918
+ .positive()
919
+ .default(constants.CUSTOM_CODE_DEFAULT_TIMEOUT_MS)
920
+ .transform(n => Math.min(n, constants.CUSTOM_CODE_MAX_TIMEOUT_MS)),
921
+ // How far ctx.resources.create and .delete reach. `own` is the default and
922
+ // the safe floor; `all` lets the script replace and remove resources it did
923
+ // not write, which is what a tool that prunes a stale crawl needs. Declared
924
+ // here so the owner grants it at publish time and the script cannot.
925
+ resourceAccess: z
926
+ .enum(constants.CUSTOM_CODE_RESOURCE_ACCESS_VALUES)
927
+ .default(constants.CUSTOM_CODE_RESOURCE_ACCESS_OWN)
928
+ });
929
+
930
+ // The same config, plus the rule that every declared connection names a provider
931
+ // we actually run a managed OAuth app for.
932
+ //
933
+ // Without this a typo is invisible until a customer's tool call fails: the
934
+ // broker answers 403 "not one of this tool's declared connections" for
935
+ // `google_gmail`, which is true and useless, because the entry IS in the list —
936
+ // it just doesn't name anything. Same split as HTTP_ENDPOINT_CONFIG_WRITE, and
937
+ // for the same reason: only the write path may tighten.
938
+ const CUSTOM_CODE_CONFIG_WRITE = CUSTOM_CODE_CONFIG.superRefine((cfg, ctx) => {
939
+ cfg.connections?.forEach((provider, index) => {
940
+ if ((constants.OAUTH_PROVIDERS as readonly string[]).includes(provider)) {
941
+ return;
942
+ }
943
+ ctx.addIssue({
944
+ code: 'custom',
945
+ path: ['connections', index],
946
+ message: constants.CUSTOM_CODE_UNKNOWN_CONNECTION_MESSAGE
947
+ });
948
+ });
949
+ });
950
+
951
+ // One tool entry in an uploaded manifest. This is the CONTRACT half of a
952
+ // version: what apps/mcp registers at boot without ever calling the dispatcher.
953
+ // `outputSchema` is optional — MCP allows a tool to declare structured output,
954
+ // and a version that omits it just returns text.
955
+ //
956
+ // This is the shape used to READ a stored version back, so it carries no
957
+ // reserved-name rule: a rule tightened after a version was published would stop
958
+ // that version registering, and the owner would see tools quietly disappear with
959
+ // only a log line to explain it. Reservation belongs to the manifest below,
960
+ // which is the write path.
961
+ const CUSTOM_CODE_TOOL = z.object({
962
+ name: z
963
+ .string()
964
+ .min(1)
965
+ .max(constants.CUSTOM_CODE_TOOL_NAME_MAX)
966
+ .regex(
967
+ /^[a-zA-Z0-9_-]+$/,
968
+ 'Tool name may only contain letters, digits, underscore or hyphen'
969
+ ),
970
+ title: z.string().min(1).max(200).optional(),
971
+ description: z.string().max(2000).optional(),
972
+ inputSchema: SCHEMA_DEFINITION.default({ type: 'object', properties: {} }),
973
+ outputSchema: SCHEMA_DEFINITION.optional()
974
+ });
975
+
976
+ // The manifest a `ganju deploy` (or the dashboard) uploads alongside a bundle.
977
+ // Names must be unique within one script: apps/mcp registers them into a single
978
+ // flat namespace, and a duplicate would silently register once and drop the rest.
979
+ // For the same reason a name may not be one the platform owns — see
980
+ // isReservedToolName.
981
+ //
982
+ // The offending entry is identified by `path` (`tools.3.name`) rather than by
983
+ // quoting the name into the message: a manifest can declare 50 tools, so "one
984
+ // of them is reserved" would not be actionable, but an interpolated message can
985
+ // never be localized — localizeZodIssue keys on the exact English string.
986
+ const CUSTOM_CODE_MANIFEST = z.object({
987
+ tools: z
988
+ .array(CUSTOM_CODE_TOOL)
989
+ .min(1, 'A version must declare at least one tool')
990
+ .max(
991
+ constants.CUSTOM_CODE_MAX_TOOLS,
992
+ `A script may declare at most ${constants.CUSTOM_CODE_MAX_TOOLS} tools`
993
+ )
994
+ .refine(tools => new Set(tools.map(t => t.name)).size === tools.length, {
995
+ message: 'Tool names must be unique within a version'
996
+ })
997
+ .superRefine((tools, ctx) => {
998
+ tools.forEach((tool, index) => {
999
+ if (isReservedToolName(tool.name)) {
1000
+ ctx.addIssue({
1001
+ code: 'custom',
1002
+ path: [index, 'name'],
1003
+ message: constants.RESERVED_TOOL_NAME_MESSAGE
1004
+ });
1005
+ }
1006
+ // Same rule as http-endpoint's write path, and here for the same
1007
+ // reason: `structuredContent` is an object, so a schema of any other
1008
+ // type promises something the tool can never return.
1009
+ if (tool.outputSchema && tool.outputSchema.type !== 'object') {
1010
+ ctx.addIssue({
1011
+ code: 'custom',
1012
+ path: [index, 'outputSchema', 'type'],
1013
+ message: constants.OUTPUT_SCHEMA_NOT_OBJECT_MESSAGE
1014
+ });
1015
+ }
1016
+ });
1017
+ })
1018
+ });
1019
+
1020
+ // Creates a draft version. Carries the manifest — the tool names and schemas the
1021
+ // MCP server will register from once this version is published — and optionally
1022
+ // the row-level config, since limits on how the code may run belong to the same
1023
+ // review as the code itself. The compiled bundle arrives separately.
1024
+ const ARTIFACT_CUSTOM_CODE_CREATE_VERSION = z.object({
1025
+ manifest: CUSTOM_CODE_MANIFEST,
1026
+ config: CUSTOM_CODE_CONFIG_WRITE.optional(),
1027
+ projectId: z.uuid(),
1028
+ userId: z.uuid(),
1029
+ organizationId: z.uuid()
1030
+ });
1031
+
1032
+ // Attaches the compiled script to a draft. Separate from the create above for
1033
+ // the same reason resource upload is separate from resource create: the bundle
1034
+ // is a raw binary body while the manifest is JSON, and one request carries one
1035
+ // body. Only the identifiers are validated here — the bytes are the body.
1036
+ const ARTIFACT_CUSTOM_CODE_UPLOAD_BUNDLE = z.object({
1037
+ versionId: z.uuid(),
1038
+ // Whether these bytes are readable source or a compiled artifact. Defaults to
1039
+ // 'bundle' because the CLI — which sends no such flag — is uploading one, and
1040
+ // because the safe answer for anything unknown is "do not offer this in a text
1041
+ // box".
1042
+ sourceKind: z
1043
+ .enum(constants.CUSTOM_CODE_SOURCE_KINDS)
1044
+ .default(constants.CUSTOM_CODE_SOURCE_KIND_BUNDLE),
1045
+ projectId: z.uuid(),
1046
+ userId: z.uuid(),
1047
+ organizationId: z.uuid()
1048
+ });
1049
+
1050
+ // Runs ONE tool of ONE version against a sample input, without publishing it.
1051
+ //
1052
+ // The version is named rather than assumed to be the draft: a test that could
1053
+ // only run the newest version would be useless the moment someone wanted to
1054
+ // check what the live one actually does. `input` is whatever the tool's own
1055
+ // input schema accepts, and is checked against it before anything is deployed.
1056
+ const ARTIFACT_CUSTOM_CODE_TEST_VERSION = z.object({
1057
+ versionId: z.uuid(),
1058
+ tool: z
1059
+ .string()
1060
+ .min(1)
1061
+ .max(constants.CUSTOM_CODE_TOOL_NAME_MAX)
1062
+ .regex(/^[a-zA-Z0-9_-]+$/, 'Tool name is not valid'),
1063
+ input: z.record(z.string(), z.any()).default({}),
1064
+ projectId: z.uuid(),
1065
+ userId: z.uuid(),
1066
+ organizationId: z.uuid()
1067
+ });
1068
+
1069
+ // Reads one version's stored source back for the editor.
1070
+ const ARTIFACT_CUSTOM_CODE_GET_SOURCE = z.object({
1071
+ versionId: z.uuid(),
1072
+ projectId: z.uuid(),
1073
+ userId: z.uuid(),
1074
+ organizationId: z.uuid()
1075
+ });
1076
+
1077
+ // Makes a draft the artifact's active version.
1078
+ const ARTIFACT_CUSTOM_CODE_PUBLISH = z.object({
1079
+ versionId: z.uuid(),
1080
+ projectId: z.uuid(),
1081
+ userId: z.uuid(),
1082
+ organizationId: z.uuid()
1083
+ });
1084
+
1085
+ // Makes a previously published (now archived) version active again. The same
1086
+ // state transition as publish with a different intent, kept apart so the two are
1087
+ // distinguishable in logs and in the UI.
1088
+ const ARTIFACT_CUSTOM_CODE_ROLLBACK = z.object({
1089
+ versionId: z.uuid(),
1090
+ projectId: z.uuid(),
1091
+ userId: z.uuid(),
1092
+ organizationId: z.uuid()
1093
+ });
1094
+
1095
+ // Recent invocations of the artifact's custom tools, newest first.
1096
+ //
1097
+ // The rows already exist — apps/mcp records one `mcp_request` per call with the
1098
+ // tool name and, for a custom tool, the `ctx.log` lines on its output. What was
1099
+ // missing is a way to read them without a database client, which is the whole of
1100
+ // what `ganju logs` needs. `tool` narrows to one name; `before` pages backwards
1101
+ // through the same ordering rather than by offset, so a row arriving mid-page
1102
+ // can't make the reader see one twice.
1103
+ const ARTIFACT_CUSTOM_CODE_LIST_LOGS = z.object({
1104
+ tool: z
1105
+ .string()
1106
+ .min(1)
1107
+ .max(constants.CUSTOM_CODE_TOOL_NAME_MAX)
1108
+ .regex(/^[a-zA-Z0-9_-]+$/, 'Tool name is not valid')
1109
+ .optional(),
1110
+ limit: z.coerce
1111
+ .number()
1112
+ .int()
1113
+ .min(1)
1114
+ .max(constants.CUSTOM_CODE_LOGS_MAX_LIMIT)
1115
+ .default(constants.CUSTOM_CODE_LOGS_DEFAULT_LIMIT),
1116
+ before: z.coerce.date().optional(),
1117
+ projectId: z.uuid(),
1118
+ userId: z.uuid(),
1119
+ organizationId: z.uuid()
1120
+ });
1121
+
1122
+ // Reads the artifact's version history and which one is currently active.
1123
+ const ARTIFACT_CUSTOM_CODE_LIST_VERSIONS = z.object({
1124
+ projectId: z.uuid(),
1125
+ userId: z.uuid(),
1126
+ organizationId: z.uuid()
1127
+ });
1128
+
1129
+ // What apps/mcp POSTs to a dispatched user script. Parsed inside the script by
1130
+ // @ganju/sdk, which routes on `tool` — one script exports many tools, so the
1131
+ // name is in the body rather than in the path.
1132
+ const CUSTOM_CODE_INVOKE_REQUEST = z.object({
1133
+ tool: z.string().min(1).max(constants.CUSTOM_CODE_TOOL_NAME_MAX),
1134
+ input: z.record(z.string(), z.unknown()).default({}),
1135
+ // Correlates the dispatcher's mcp_request row with whatever the script logs.
1136
+ // Opaque to user code.
1137
+ requestId: z.string().max(100).optional()
1138
+ });
1139
+
1140
+ // What the script returns. `output` is validated against the version's declared
1141
+ // outputSchema by the dispatcher, not here — this schema only establishes the
1142
+ // envelope, so a script that returns garbage produces a legible tool error
1143
+ // instead of an unhandled parse failure in the MCP worker.
1144
+ //
1145
+ // `error` is how a script reports a handled failure. It travels as a 200 with an
1146
+ // error field rather than an HTTP status, because a non-2xx from the dispatch
1147
+ // namespace is ambiguous — it could equally be the platform refusing to run the
1148
+ // script at all.
1149
+ const CUSTOM_CODE_INVOKE_RESPONSE = z.object({
1150
+ output: z.unknown().optional(),
1151
+ logs: z
1152
+ .array(
1153
+ z.object({
1154
+ level: z.enum(['log', 'warn', 'error']).default('log'),
1155
+ message: z.string().max(constants.CUSTOM_CODE_MAX_LOG_LENGTH)
1156
+ })
1157
+ )
1158
+ .max(constants.CUSTOM_CODE_MAX_LOGS)
1159
+ .default([]),
1160
+ error: z.string().max(2000).optional()
1161
+ });
1162
+
1163
+ // Broker request bodies. Every one of these is authenticated by the bearer token
1164
+ // alone — no body field names the artifact, because a script could lie about it.
1165
+ const CUSTOM_CODE_BROKER_CONNECTION = z.object({
1166
+ provider: z.string().min(1).max(100)
1167
+ });
1168
+
1169
+ const CUSTOM_CODE_BROKER_SECRET = z.object({
1170
+ // The credential's label, which is the lookup key for custom-code secrets
1171
+ // (they carry no id inside the script). See CREDENTIAL_PROVIDER_CUSTOM_CODE.
1172
+ name: z.string().min(1).max(200)
1173
+ });
1174
+
1175
+ const CUSTOM_CODE_BROKER_RESOURCE_SEARCH = z.object({
1176
+ query: z.string().min(1).max(2000),
1177
+ limit: z.number().int().positive().max(20).default(5)
1178
+ });
1179
+
1180
+ const CUSTOM_CODE_BROKER_RESOURCE_READ = z.object({
1181
+ uri: z.string().min(1).max(2000)
1182
+ });
1183
+
1184
+ // One ctx.resources.delete call. Same shape as a read — a uri is the only thing
1185
+ // a script holds — but its own schema so the two can diverge without one
1186
+ // quietly accepting what the other meant.
1187
+ const CUSTOM_CODE_BROKER_RESOURCE_DELETE = z.object({
1188
+ uri: z.string().min(1).max(2000),
1189
+ // Opt-in rather than implied. The FK cascades regardless, so a delete that
1190
+ // took children silently would be the difference between removing one page
1191
+ // and removing a 400-page crawl — a distinction the caller has to make on
1192
+ // purpose.
1193
+ children: z.boolean().default(false)
1194
+ });
1195
+
1196
+ // One ctx.resources.create call.
1197
+ //
1198
+ // `content` and `bytes` are the two payload shapes and exactly one is required:
1199
+ // text goes inline on the row, bytes (base64) go to storage. Requiring one and
1200
+ // refusing both is what keeps the row's meaning unambiguous — a resource with
1201
+ // inline text AND a stored object has two answers to "what is in it", and every
1202
+ // reader would have to pick.
1203
+ //
1204
+ // `mimeType` is constrained to the same list an uploaded file is, so a script
1205
+ // cannot introduce a type no other surface knows how to render or attach.
1206
+ const CUSTOM_CODE_BROKER_RESOURCE_CREATE = z
1207
+ .object({
1208
+ title: z.string().min(1).max(200),
1209
+ // Optional: derived from the title when omitted. A script that re-runs and
1210
+ // wants to replace its previous output passes the same one.
1211
+ uri: z.string().min(1).max(2000).optional(),
1212
+ description: z.string().max(1000).optional(),
1213
+ mimeType: z
1214
+ .enum(constants.MIMETYPES, { message: 'Unsupported mime type' })
1215
+ .optional(),
1216
+ content: z.string().optional(),
1217
+ bytes: z.string().optional(),
1218
+ // Only meaningful for the bytes path; drives the stored object's name and
1219
+ // the filename an attachment arrives under.
1220
+ fileName: z.string().min(1).max(255).optional(),
1221
+ // Put this resource in the artifact's search corpus. Off by default: script
1222
+ // output that lands in the corpus is content the assistant will answer other
1223
+ // people's questions from, and that should be a decision rather than a
1224
+ // side effect of writing a file.
1225
+ index: z.boolean().default(false)
1226
+ })
1227
+ .superRefine((value, ctx) => {
1228
+ const hasContent = value.content !== undefined;
1229
+ const hasBytes = value.bytes !== undefined;
1230
+
1231
+ if (hasContent === hasBytes) {
1232
+ ctx.addIssue({
1233
+ code: 'custom',
1234
+ path: [hasContent ? 'bytes' : 'content'],
1235
+ message: constants.CUSTOM_CODE_RESOURCE_PAYLOAD_MESSAGE
1236
+ });
1237
+ return;
1238
+ }
1239
+
1240
+ // Measured in bytes rather than characters: the cap exists to bound what is
1241
+ // stored, and a string of emoji is four times its length on the way to
1242
+ // Postgres.
1243
+ if (hasContent) {
1244
+ const size = new TextEncoder().encode(value.content).byteLength;
1245
+ if (size > constants.CUSTOM_CODE_MAX_RESOURCE_TEXT_BYTES) {
1246
+ ctx.addIssue({
1247
+ code: 'custom',
1248
+ path: ['content'],
1249
+ message: constants.CUSTOM_CODE_RESOURCE_TEXT_TOO_LARGE_MESSAGE
1250
+ });
1251
+ }
1252
+ return;
1253
+ }
1254
+
1255
+ // The base64 length bounds the decoded length, so this rejects an oversized
1256
+ // payload without decoding it first — the decode is what would actually cost
1257
+ // the memory being defended.
1258
+ const decodedSize = Math.floor((value.bytes!.length * 3) / 4);
1259
+ if (decodedSize > constants.CUSTOM_CODE_MAX_RESOURCE_FILE_BYTES) {
1260
+ ctx.addIssue({
1261
+ code: 'custom',
1262
+ path: ['bytes'],
1263
+ message: constants.CUSTOM_CODE_RESOURCE_FILE_TOO_LARGE_MESSAGE
1264
+ });
1265
+ }
1266
+ });
1267
+
1268
+ // The resources to deliver. Named by URI rather than by id because a URI is what
1269
+ // every other resource surface a script can see hands back — ctx.resources.list
1270
+ // and .search both return one, and neither returns a row id.
1271
+ const CUSTOM_CODE_SEND_FILE_URIS = z
1272
+ .array(z.string().min(1).max(2000))
1273
+ .min(1, 'At least one resource uri is required')
1274
+ .max(
1275
+ constants.CUSTOM_CODE_SEND_FILE_MAX_URIS,
1276
+ `At most ${constants.CUSTOM_CODE_SEND_FILE_MAX_URIS} resources can be sent in one call`
1277
+ );
1278
+
1279
+ const CUSTOM_CODE_SEND_FILE_BODY = z.string().max(100_000);
1280
+ const CUSTOM_CODE_SEND_FILE_RECIPIENTS = z.string().min(1).max(2000);
1281
+
1282
+ // What ctx.sendFile posts to the broker.
1283
+ //
1284
+ // Discriminated on `to` because the three destinations genuinely disagree about
1285
+ // what a send is: two are mail with recipients and a subject, one is a channel
1286
+ // upload with a comment. A single flat shape with everything optional would
1287
+ // accept `{ to: 'slack', subject: '…' }` and drop the subject silently.
1288
+ //
1289
+ // Each variant deliberately covers ONE operation — a new mail, a new upload.
1290
+ // The container routes behind them can also reply, forward, and update drafts,
1291
+ // but sendFile exists to move bytes a script cannot hold, not to be a mail
1292
+ // client: anything else a script wants from these APIs it can do itself with
1293
+ // ctx.connection() and a fetch, where its own allow-list applies.
1294
+ const CUSTOM_CODE_SEND_FILE = z.discriminatedUnion('to', [
1295
+ z.object({
1296
+ to: z.literal(constants.CUSTOM_CODE_SEND_FILE_TARGET_GMAIL),
1297
+ uris: CUSTOM_CODE_SEND_FILE_URIS,
1298
+ message: z.object({
1299
+ // Gmail's own field name for the recipient; the destination lives on `to`
1300
+ // one level up.
1301
+ to: CUSTOM_CODE_SEND_FILE_RECIPIENTS,
1302
+ subject: z.string().max(2000).optional(),
1303
+ body: CUSTOM_CODE_SEND_FILE_BODY.default(''),
1304
+ cc: z.string().max(2000).optional(),
1305
+ bcc: z.string().max(2000).optional(),
1306
+ contentType: z.enum(['text/html', 'text/plain']).optional(),
1307
+ // Threads an attachment onto an existing conversation, which a script
1308
+ // typically got from a gmail- tool call earlier in the same turn.
1309
+ threadId: z.string().max(200).optional()
1310
+ })
1311
+ }),
1312
+ z.object({
1313
+ to: z.literal(constants.CUSTOM_CODE_SEND_FILE_TARGET_OUTLOOK),
1314
+ uris: CUSTOM_CODE_SEND_FILE_URIS,
1315
+ message: z.object({
1316
+ to: CUSTOM_CODE_SEND_FILE_RECIPIENTS,
1317
+ subject: z.string().max(2000).optional(),
1318
+ body: CUSTOM_CODE_SEND_FILE_BODY.default(''),
1319
+ cc: z.string().max(2000).optional(),
1320
+ bcc: z.string().max(2000).optional(),
1321
+ // Graph's vocabulary, not a MIME type — the container passes it straight
1322
+ // through to the message payload.
1323
+ contentType: z.enum(['html', 'text']).optional()
1324
+ })
1325
+ }),
1326
+ z.object({
1327
+ to: z.literal(constants.CUSTOM_CODE_SEND_FILE_TARGET_SLACK),
1328
+ // Exactly one, unlike the two mail destinations. Slack's external-upload
1329
+ // flow moves a single file per call — three round trips per file — and the
1330
+ // container route behind it reads one `attachment` field. Accepting a list
1331
+ // here would take ten and deliver the first, silently. A script sending
1332
+ // several files calls sendFile several times.
1333
+ uris: CUSTOM_CODE_SEND_FILE_URIS.length(
1334
+ 1,
1335
+ 'Slack delivers one file per call'
1336
+ ),
1337
+ message: z.object({
1338
+ channel: z.string().min(1).max(200),
1339
+ title: z.string().max(200).optional(),
1340
+ initialComment: z.string().max(4000).optional(),
1341
+ threadTs: z.string().max(100).optional()
1342
+ })
1343
+ })
1344
+ ]);
1345
+
1346
+ const ARTIFACT_UPDATE_RESOURCE_SHOW_SOURCE = z.object({
1347
+ resourceId: z.uuid(),
1348
+ showSource: z.enum(constants.CHANNEL_STATUS),
1349
+ projectId: z.uuid(),
1350
+ userId: z.uuid(),
1351
+ organizationId: z.uuid()
1352
+ });
1353
+
1354
+ const ARTIFACT_GET = z.object({
1355
+ projectId: z.uuid(),
1356
+ userId: z.uuid(),
1357
+ organizationId: z.uuid()
1358
+ });
1359
+
1360
+ const ARTIFACT_UPDATE_SLUG = z.object({
1361
+ slug: z
1362
+ .string()
1363
+ .trim()
1364
+ .toLowerCase()
1365
+ .refine(isValidSlugFormat, {
1366
+ message:
1367
+ 'Slug must be 3–63 chars, lowercase letters, digits, or hyphens, and start/end with a letter or digit'
1368
+ })
1369
+ .refine(s => !isReservedSlug(s), { message: 'Slug is reserved' }),
1370
+ projectId: z.uuid(),
1371
+ userId: z.uuid(),
1372
+ organizationId: z.uuid()
1373
+ });
1374
+
1375
+ const CHANNEL_GET = z.object({
1376
+ projectId: z.uuid(),
1377
+ userId: z.uuid(),
1378
+ organizationId: z.uuid()
1379
+ });
1380
+
1381
+ const CHANNEL_REMOVE = z.object({
1382
+ channelId: z.uuid(),
1383
+ projectId: z.uuid(),
1384
+ userId: z.uuid(),
1385
+ organizationId: z.uuid()
1386
+ });
1387
+
1388
+ const CHANNEL_LIST_CONVERSATIONS = z.object({
1389
+ channelId: z.uuid(),
1390
+ projectId: z.uuid(),
1391
+ userId: z.uuid(),
1392
+ organizationId: z.uuid()
1393
+ });
1394
+
1395
+ const CHANNEL_LIST_MESSAGES = z.object({
1396
+ channelId: z.uuid(),
1397
+ conversationId: z.uuid(),
1398
+ projectId: z.uuid(),
1399
+ userId: z.uuid(),
1400
+ organizationId: z.uuid()
1401
+ });
1402
+
1403
+ const CHANNEL_CREATE_VIEW = CHANNEL_CREATE.omit({
1404
+ projectId: true,
1405
+ userId: true,
1406
+ organizationId: true
1407
+ });
1408
+
1409
+ const CHANNEL_UPDATE_VIEW = CHANNEL_UPDATE.omit({
1410
+ channelId: true,
1411
+ projectId: true,
1412
+ userId: true,
1413
+ organizationId: true
1414
+ });
1415
+
1416
+ const ARTIFACT_CREATE_PROMPT_VIEW = ARTIFACT_CREATE_PROMPT.omit({
1417
+ projectId: true,
1418
+ userId: true,
1419
+ organizationId: true
1420
+ });
1421
+
1422
+ const ARTIFACT_UPDATE_PROMPT_VIEW = ARTIFACT_UPDATE_PROMPT.omit({
1423
+ promptId: true,
1424
+ projectId: true,
1425
+ userId: true,
1426
+ organizationId: true
1427
+ });
1428
+
1429
+ const ARTIFACT_CREATE_RESOURCE_VIEW = ARTIFACT_CREATE_RESOURCE.omit({
1430
+ projectId: true,
1431
+ userId: true,
1432
+ organizationId: true
1433
+ });
1434
+
1435
+ const ARTIFACT_UPDATE_RESOURCE_VIEW = ARTIFACT_UPDATE_RESOURCE.omit({
1436
+ resourceId: true,
1437
+ projectId: true,
1438
+ userId: true,
1439
+ organizationId: true
1440
+ });
1441
+
1442
+ const ARTIFACT_UPDATE_SLUG_VIEW = ARTIFACT_UPDATE_SLUG.omit({
1443
+ projectId: true,
1444
+ userId: true,
1445
+ organizationId: true
1446
+ });
1447
+
1448
+ export const Schema = {
1449
+ CONTACT_MESSAGE,
1450
+ ORGANIZATION_CREATE,
1451
+ ORGANIZATION_CREATE_VIEW,
1452
+ ORGANIZATION_UPDATE,
1453
+ ORGANIZATION_GET,
1454
+ AUTH_USER_GET,
1455
+ PROJECT_CREATE,
1456
+ PROJECT_CREATE_VIEW,
1457
+ PROJECT_UPDATE,
1458
+ PROJECT_GET,
1459
+ ORGANIZATION_INVITATION_CREATE,
1460
+ ORGANIZATION_INVITATION_CREATE_VIEW,
1461
+ ORGANIZATION_INVITATION_LIST,
1462
+ ORGANIZATION_INVITATION_REMOVE,
1463
+ PROJECT_INVITATION_CREATE,
1464
+ PROJECT_INVITATION_CREATE_VIEW,
1465
+ PROJECT_INVITATION_LIST,
1466
+ PROJECT_INVITATION_REMOVE,
1467
+ INVITATION_RESPOND,
1468
+ INVITATION_GET_BY_TOKEN,
1469
+ ORGANIZATION_MEMBER_LIST,
1470
+ ORGANIZATION_MEMBER_REMOVE,
1471
+ PROJECT_MEMBER_LIST,
1472
+ PROJECT_MEMBER_REMOVE,
1473
+ ARTIFACT_CREATE_PROMPT,
1474
+ ARTIFACT_CREATE_PROMPT_VIEW,
1475
+ ARTIFACT_UPDATE_PROMPT,
1476
+ ARTIFACT_UPDATE_PROMPT_VIEW,
1477
+ ARTIFACT_GET_PROMPT,
1478
+ ARTIFACT_REMOVE_PROMPT,
1479
+ ARTIFACT_CREATE_RESOURCE,
1480
+ ARTIFACT_CREATE_RESOURCE_VIEW,
1481
+ ARTIFACT_CREATE_WEBSITE,
1482
+ ARTIFACT_CREATE_WEBSITE_VIEW,
1483
+ ARTIFACT_UPDATE_WEBSITE,
1484
+ ARTIFACT_UPDATE_WEBSITE_VIEW,
1485
+ ARTIFACT_CREATE_GOOGLE_DRIVE,
1486
+ ARTIFACT_CREATE_GOOGLE_DRIVE_VIEW,
1487
+ ARTIFACT_SYNC_GOOGLE_DRIVE,
1488
+ ARTIFACT_CREATE_ONE_DRIVE,
1489
+ ARTIFACT_CREATE_ONE_DRIVE_VIEW,
1490
+ ARTIFACT_SYNC_ONE_DRIVE,
1491
+ ARTIFACT_UPDATE_RESOURCE,
1492
+ ARTIFACT_UPDATE_RESOURCE_VIEW,
1493
+ ARTIFACT_GET_RESOURCE,
1494
+ ARTIFACT_GET_RESOURCE_BY_ID,
1495
+ ARTIFACT_REMOVE_RESOURCE,
1496
+ ARTIFACT_UPLOAD_RESOURCE_FILE,
1497
+ ARTIFACT_DOWNLOAD_RESOURCE_FILE,
1498
+ ARTIFACT_CREATE_TOOL,
1499
+ ARTIFACT_UPDATE_TOOL,
1500
+ ARTIFACT_GET_TOOL,
1501
+ ARTIFACT_REMOVE_TOOL,
1502
+ ARTIFACT_SET_TOOL_ENABLED,
1503
+ ARTIFACT_GET_CREDENTIAL,
1504
+ ARTIFACT_LIST_CONNECTIONS,
1505
+ ARTIFACT_REMOVE_CREDENTIAL,
1506
+ ARTIFACT_CREATE_CREDENTIAL,
1507
+ ARTIFACT_GET,
1508
+ ARTIFACT_UPDATE_SLUG,
1509
+ ARTIFACT_UPDATE_SLUG_VIEW,
1510
+ ORGANIZATION_LIST_LLM,
1511
+ ORGANIZATION_CREATE_LLM,
1512
+ ORGANIZATION_CREATE_LLM_VIEW,
1513
+ ORGANIZATION_UPDATE_LLM,
1514
+ ORGANIZATION_UPDATE_LLM_VIEW,
1515
+ ORGANIZATION_REMOVE_LLM,
1516
+ CHANNEL_CONFIG,
1517
+ CHANNEL_CREATE,
1518
+ CHANNEL_CREATE_VIEW,
1519
+ CHANNEL_UPDATE,
1520
+ CHANNEL_UPDATE_VIEW,
1521
+ CHANNEL_GET,
1522
+ CHANNEL_REMOVE,
1523
+ CHANNEL_LIST_CONVERSATIONS,
1524
+ CHANNEL_LIST_MESSAGES,
1525
+ ARTIFACT_UPDATE_RESOURCE_SHOW_SOURCE,
1526
+ HTTP_ENDPOINT_CONFIG,
1527
+ HTTP_ENDPOINT_CONFIG_WRITE,
1528
+ MCP_PROXY_CONFIG,
1529
+ CUSTOM_CODE_CONFIG,
1530
+ CUSTOM_CODE_CONFIG_WRITE,
1531
+ CUSTOM_CODE_TOOL,
1532
+ CUSTOM_CODE_MANIFEST,
1533
+ ARTIFACT_CUSTOM_CODE_CREATE_VERSION,
1534
+ ARTIFACT_CUSTOM_CODE_UPLOAD_BUNDLE,
1535
+ ARTIFACT_CUSTOM_CODE_GET_SOURCE,
1536
+ ARTIFACT_CUSTOM_CODE_TEST_VERSION,
1537
+ ARTIFACT_CUSTOM_CODE_PUBLISH,
1538
+ ARTIFACT_CUSTOM_CODE_ROLLBACK,
1539
+ ARTIFACT_CUSTOM_CODE_LIST_LOGS,
1540
+ ARTIFACT_CUSTOM_CODE_LIST_VERSIONS,
1541
+ CUSTOM_CODE_INVOKE_REQUEST,
1542
+ CUSTOM_CODE_INVOKE_RESPONSE,
1543
+ CUSTOM_CODE_BROKER_CONNECTION,
1544
+ CUSTOM_CODE_BROKER_SECRET,
1545
+ CUSTOM_CODE_BROKER_RESOURCE_SEARCH,
1546
+ CUSTOM_CODE_BROKER_RESOURCE_READ,
1547
+ CUSTOM_CODE_BROKER_RESOURCE_CREATE,
1548
+ CUSTOM_CODE_BROKER_RESOURCE_DELETE,
1549
+ CUSTOM_CODE_SEND_FILE
1550
+ };
1551
+
1552
+ // Fully-resolved http-endpoint config (post-parse, defaults applied).
1553
+ export type HttpEndpointToolConfig = z.infer<typeof HTTP_ENDPOINT_CONFIG>;
1554
+
1555
+ // Fully-resolved mcp-proxy config (post-parse, defaults applied).
1556
+ export type McpProxyToolConfig = z.infer<typeof MCP_PROXY_CONFIG>;
1557
+
1558
+ // Fully-resolved custom-code config (post-parse, defaults applied). Holds the
1559
+ // pointer to the active version plus the row-level limits — never the tools.
1560
+ export type CustomCodeToolConfig = z.infer<typeof CUSTOM_CODE_CONFIG>;
1561
+
1562
+ // One tool a custom-code version declares. Persisted verbatim on
1563
+ // artifact_tool_version.tools, which is what the MCP boot loop reads — the same
1564
+ // configure-time-discovery trick mcp-proxy uses, except the source of truth is
1565
+ // the user's manifest rather than a remote server.
1566
+ export type CustomCodeToolManifest = z.infer<typeof CUSTOM_CODE_TOOL>;
1567
+
1568
+ // The full manifest uploaded with a bundle.
1569
+ export type CustomCodeManifest = z.infer<typeof CUSTOM_CODE_MANIFEST>;
1570
+
1571
+ // One ctx.sendFile call, after parsing. The SDK's SendFileOptions is the same
1572
+ // union written by hand — it can't import this one, because @ganju/sdk is
1573
+ // bundled into every uploaded script and reaching the barrel would ship zod
1574
+ // with it.
1575
+ export type CustomCodeSendFile = z.infer<typeof CUSTOM_CODE_SEND_FILE>;
1576
+
1577
+ // One ctx.resources.create call, after parsing. Same split as above: the SDK
1578
+ // declares the input shape by hand so it never has to import zod.
1579
+ export type CustomCodeCreateResource = z.infer<
1580
+ typeof CUSTOM_CODE_BROKER_RESOURCE_CREATE
1581
+ >;
1582
+
1583
+ // One remote tool discovered from a proxied MCP server. Stored on
1584
+ // artifact_tool.metadata.discovery at configure-time so the stateless MCP boot
1585
+ // loop can register the tool without a remote round-trip.
1586
+ export interface McpProxyDiscoveredTool {
1587
+ name: string;
1588
+ title?: string;
1589
+ description?: string;
1590
+ inputSchema: unknown;
1591
+ }
1592
+
1593
+ // One remote resource discovered from a proxied MCP server. Identified by uri;
1594
+ // registration is deferred, so this is currently only surfaced to the UI for
1595
+ // enable/disable selection.
1596
+ export interface McpProxyDiscoveredResource {
1597
+ uri: string;
1598
+ name?: string;
1599
+ title?: string;
1600
+ description?: string;
1601
+ mimeType?: string;
1602
+ }
1603
+
1604
+ // One remote prompt discovered from a proxied MCP server. Identified by name.
1605
+ // `arguments` (from the remote's prompts/list) drives the local prompt's
1606
+ // argument schema at boot — MCP prompt arguments are always strings.
1607
+ export interface McpProxyDiscoveredPrompt {
1608
+ name: string;
1609
+ title?: string;
1610
+ description?: string;
1611
+ arguments?: { name: string; description?: string; required?: boolean }[];
1612
+ }
1613
+
1614
+ // The discovery payload persisted on artifact_tool.metadata.discovery — the
1615
+ // FULL set of items the remote exposes (the enabled subset lives in config's
1616
+ // allowed* lists). Tools, resources, and prompts are all registered at boot
1617
+ // (resources/prompts only for the opt-in subset named in the allow-lists).
1618
+ export interface McpProxyDiscovery {
1619
+ discoveredAt: string;
1620
+ serverInfo?: { name?: string; version?: string };
1621
+ tools: McpProxyDiscoveredTool[];
1622
+ resources?: McpProxyDiscoveredResource[];
1623
+ prompts?: McpProxyDiscoveredPrompt[];
1624
+ }