@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
@@ -0,0 +1,44 @@
1
+ // The handful of constants that get bundled into every customer's deployed
2
+ // script, split out from the main constants module for exactly that reason.
3
+ //
4
+ // `@ganju/utils/constants` is one large object literal, so a bundler cannot drop
5
+ // the keys a consumer doesn't touch — importing it from the SDK pulled ~70KB of
6
+ // plan limits, MIME tables and vendor scopes into every uploaded Worker to reach
7
+ // fourteen strings. These live here instead, and the main module re-exports them
8
+ // so there is still one definition of each.
9
+ //
10
+ // The rule for this file: nothing lands here unless the SDK reads it at runtime,
11
+ // and it imports nothing.
12
+
13
+ // Bindings injected into every user script at upload time. The token is a
14
+ // secret_text binding rotated on each publish; the broker is a service binding.
15
+ // Names are part of the SDK's contract with the script, so they can't be
16
+ // renamed without breaking already-deployed code.
17
+ export const CUSTOM_CODE_BINDING_TOKEN = 'GANJU_TOOL_TOKEN';
18
+ export const CUSTOM_CODE_BINDING_BROKER = 'GANJU_BROKER';
19
+
20
+ // Broker routes, called by user code through the service binding above with
21
+ // `Authorization: Bearer <GANJU_TOOL_TOKEN>`. The origin is arbitrary — a
22
+ // service binding ignores the hostname.
23
+ export const CUSTOM_CODE_BROKER_ORIGIN = 'https://broker.ganju.internal';
24
+ export const CUSTOM_CODE_BROKER_PATH_CONNECTION = '/connection';
25
+ export const CUSTOM_CODE_BROKER_PATH_SECRET = '/secret';
26
+ export const CUSTOM_CODE_BROKER_PATH_RESOURCES_SEARCH = '/resources/search';
27
+ export const CUSTOM_CODE_BROKER_PATH_RESOURCES_READ = '/resources/read';
28
+ export const CUSTOM_CODE_BROKER_PATH_RESOURCES_LIST = '/resources/list';
29
+ export const CUSTOM_CODE_BROKER_PATH_RESOURCES_CREATE = '/resources/create';
30
+ export const CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE = '/resources/delete';
31
+ export const CUSTOM_CODE_BROKER_PATH_SEND_FILE = '/send-file';
32
+
33
+ // The reserved tool name the publish pipeline calls against a freshly uploaded
34
+ // script to ask which tools it actually exports — which is what turns "the
35
+ // manifest declares lookup-order but the code exports lookupOrder" from a
36
+ // call-time mystery into a publish-time error. Underscore-prefixed so it can
37
+ // never collide with a declared name.
38
+ export const CUSTOM_CODE_HEALTH_TOOL = '__ganju_health';
39
+
40
+ // How many ctx.log() lines travel back with a result. Logs are buffered inside
41
+ // the isolate and returned in the response rather than sent to the broker line
42
+ // by line — a log call shouldn't cost a network round trip.
43
+ export const CUSTOM_CODE_MAX_LOGS = 50;
44
+ export const CUSTOM_CODE_MAX_LOG_LENGTH = 2_000;
@@ -0,0 +1,65 @@
1
+ // Wire protocol shared between the MCP worker (which builds the request) and
2
+ // the resource-handler container (which talks to the Slack Web API).
3
+ //
4
+ // post-message: simple chat.postMessage. No attachments. Worker could call
5
+ // Slack directly, but we route through the container to keep one auth/error
6
+ // path for the slack provider and so future block-kit composition can grow
7
+ // without bloating the worker bundle.
8
+ //
9
+ // upload-file: Slack's new external-upload flow is three round trips
10
+ // (files.getUploadURLExternal → PUT bytes → files.completeUploadExternal).
11
+ // The bytes ride in as a multipart `attachment` field; the container
12
+ // streams them out to Slack's signed URL.
13
+
14
+ export type SlackOperation = 'post-message' | 'upload-file';
15
+
16
+ export interface SlackSendRequest {
17
+ accessToken: string;
18
+ operation: SlackOperation;
19
+
20
+ // chat.postMessage / files.completeUploadExternal share these
21
+ channel: string; // channel ID (C…/G…/D…) or name (#general)
22
+ threadTs?: string; // reply in-thread when set
23
+
24
+ // post-message
25
+ text?: string; // required for post-message
26
+ mrkdwn?: boolean; // defaults to true on Slack's side
27
+ blocks?: unknown[]; // optional Block Kit payload
28
+
29
+ // upload-file
30
+ title?: string; // shown in the Slack file viewer
31
+ initialComment?: string; // message body posted alongside the file
32
+ }
33
+
34
+ // Wire protocol for sending a PROXIED (remote MCP) resource as a Slack file.
35
+ // Mirrors TelegramSendRemoteResourceRequest: the worker sends only the remote
36
+ // connection details + resolved auth header as JSON, and the resource-handler
37
+ // container does the remote read + decode + Slack external-upload itself — so a
38
+ // large file's bytes never transit the 128 MiB worker.
39
+ export interface SlackSendRemoteResourceRequest {
40
+ slack: {
41
+ accessToken: string;
42
+ channel: string;
43
+ threadTs?: string;
44
+ title?: string;
45
+ initialComment?: string;
46
+ };
47
+ remote: {
48
+ url: string;
49
+ transport: string;
50
+ // Single header injected on the remote MCP connection (e.g. Authorization).
51
+ authHeader?: { name: string; value: string } | null;
52
+ uri: string;
53
+ timeoutMs: number;
54
+ };
55
+ }
56
+
57
+ export interface SlackSendResponse {
58
+ // chat.postMessage returns ts; files.completeUploadExternal returns the
59
+ // file id. Empty string when Slack returns ok:true with no id (shouldn't
60
+ // happen, but defensively typed).
61
+ id: string;
62
+ channel?: string; // resolved channel id
63
+ ts?: string; // message timestamp (post-message only)
64
+ permalink?: string; // file permalink (upload-file only)
65
+ }
package/src/slug.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { constants } from './constants';
2
+
3
+ const SLUG_BYTES = 16;
4
+ const SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
5
+ const RESERVED = new Set(constants.RESERVED_SLUGS);
6
+
7
+ const toHex = (bytes: Uint8Array): string => {
8
+ let hex = '';
9
+ for (const b of bytes) {
10
+ hex += b.toString(16).padStart(2, '0');
11
+ }
12
+ return hex;
13
+ };
14
+
15
+ export const generateRandomSlug = (): string => {
16
+ const bytes = new Uint8Array(SLUG_BYTES);
17
+ crypto.getRandomValues(bytes);
18
+ return toHex(bytes);
19
+ };
20
+
21
+ // Opaque, unguessable token — used as the public handle for an invitation.
22
+ export const generateRandomToken = (
23
+ byteLength = constants.INVITATION_TOKEN_BYTES
24
+ ): string => {
25
+ const bytes = new Uint8Array(byteLength);
26
+ crypto.getRandomValues(bytes);
27
+ return toHex(bytes);
28
+ };
29
+
30
+ export const isReservedSlug = (slug: string): boolean =>
31
+ RESERVED.has(slug.toLowerCase());
32
+
33
+ export const isValidSlugFormat = (slug: string): boolean =>
34
+ typeof slug === 'string' &&
35
+ slug.length >= 3 &&
36
+ slug.length <= 63 &&
37
+ SLUG_PATTERN.test(slug);
@@ -0,0 +1,8 @@
1
+ // Lowercase a human title into a slug usable as a command name / MCP id —
2
+ // shared by prompts and resources (anything titled), so it's deliberately
3
+ // generic rather than prompt-specific.
4
+ export const slugifyTitle = (title: string): string =>
5
+ title
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, '_')
8
+ .replace(/^_+|_+$/g, '');
package/src/sources.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { constants } from './constants';
2
+
3
+ export interface Source {
4
+ resourceId: string;
5
+ uri: string;
6
+ title: string;
7
+ sourceType: 'FILE' | 'WEBSITE' | 'CUSTOM_CODE';
8
+ mimeType: string;
9
+ fileName: string | null;
10
+ pageNumber?: number;
11
+ chunkIndex?: number;
12
+ score?: number;
13
+ excerpt?: string;
14
+ }
15
+
16
+ export interface ResourceUrlContext {
17
+ apiUrl: string;
18
+ organizationId: string;
19
+ projectId: string;
20
+ }
21
+
22
+ /**
23
+ * Does this source resolve to bytes we serve, rather than to a URL a reader can
24
+ * open?
25
+ *
26
+ * The distinction the two formatters below need is "download link or web link",
27
+ * and for a long time that was the same question as "is it a FILE". It stopped
28
+ * being once a tool could write a resource: a script-created one holds bytes
29
+ * exactly as an upload does, and its uri is a `resource://` address that means
30
+ * nothing to a browser. Formatting it as a web link produced a dead button in
31
+ * whichever channel the answer went to.
32
+ */
33
+ export const isDownloadableSource = (sourceType: Source['sourceType']): boolean =>
34
+ sourceType === constants.RESOURCE_SOURCE_TYPE_FILE ||
35
+ sourceType === constants.RESOURCE_SOURCE_TYPE_CUSTOM_CODE;
36
+
37
+ export const isResourceSourceEnabled = (
38
+ resource: { showSource?: string | null } | null | undefined
39
+ ): boolean =>
40
+ (resource?.showSource ?? constants.STATUS_ACTIVE) !==
41
+ constants.STATUS_DISABLED;
42
+
43
+ export const safeHostname = (url: string): string | null => {
44
+ if (!URL.canParse(url)) return null;
45
+ return new URL(url).hostname.replace(/^www\./, '');
46
+ };
47
+
48
+ export const buildResourceDownloadUrl = (
49
+ ctx: ResourceUrlContext,
50
+ resourceId: string,
51
+ pageNumber?: number
52
+ ): string => {
53
+ const fragment = pageNumber ? `#page=${pageNumber}` : '';
54
+ return `${ctx.apiUrl}/organization/${ctx.organizationId}/project/${ctx.projectId}/artifact/resource/${resourceId}/download${fragment}`;
55
+ };
56
+
57
+ export interface SourceButton {
58
+ text: string;
59
+ url: string;
60
+ }
61
+
62
+ const truncateLabel = (text: string, max: number): string =>
63
+ text.length <= max ? text : `${text.slice(0, max - 1)}…`;
64
+
65
+ export const formatSourcesAsButtons = (
66
+ sources: Source[],
67
+ ctx: ResourceUrlContext,
68
+ options?: { maxLabelLength?: number }
69
+ ): SourceButton[] => {
70
+ const maxLabelLength = options?.maxLabelLength ?? 60;
71
+ return sources.map((source, index) => {
72
+ const position = index + 1;
73
+ if (isDownloadableSource(source.sourceType)) {
74
+ const label = source.fileName || source.title;
75
+ const pageSuffix = source.pageNumber ? ` · p.${source.pageNumber}` : '';
76
+ const url = buildResourceDownloadUrl(
77
+ ctx,
78
+ source.resourceId,
79
+ source.pageNumber
80
+ );
81
+ return {
82
+ text: truncateLabel(`${position} ${label}${pageSuffix}`, maxLabelLength),
83
+ url
84
+ };
85
+ }
86
+ const domain = safeHostname(source.uri);
87
+ const base = domain ? `${domain} — ${source.title}` : source.title;
88
+ return {
89
+ text: truncateLabel(`${position} ${base}`, maxLabelLength),
90
+ url: source.uri
91
+ };
92
+ });
93
+ };
94
+
95
+ export const formatSourcesAsMarkdown = (
96
+ sources: Source[],
97
+ ctx: ResourceUrlContext
98
+ ): string => {
99
+ if (sources.length === 0) return '';
100
+ const lines = sources.map((source, index) => {
101
+ const position = index + 1;
102
+ if (isDownloadableSource(source.sourceType)) {
103
+ const label = source.fileName || source.title;
104
+ const pageSuffix = source.pageNumber ? ` · p. ${source.pageNumber}` : '';
105
+ const url = buildResourceDownloadUrl(
106
+ ctx,
107
+ source.resourceId,
108
+ source.pageNumber
109
+ );
110
+ return `${position}. [${label}${pageSuffix}](${url})`;
111
+ }
112
+ const domain = safeHostname(source.uri);
113
+ const label = domain ? `${domain} — ${source.title}` : source.title;
114
+ return `${position}. [${label}](${source.uri})`;
115
+ });
116
+ return `**Sources**\n${lines.join('\n')}`;
117
+ };
package/src/ssrf.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Literal-host SSRF screening shared by the proxied tool dispatchers
2
+ // (http-endpoint, mcp-proxy). The Workers runtime can't resolve DNS, so we can
3
+ // only screen literal hosts and IPs (not DNS rebinding). Private/loopback/
4
+ // link-local ranges are always rejected; callers may further narrow with an
5
+ // allowlist.
6
+ export const ipv4InPrivateRange = (host: string): boolean => {
7
+ const parts = host.split('.');
8
+ if (parts.length !== 4) return false;
9
+ const octets = parts.map(p => Number(p));
10
+ if (octets.some(o => !Number.isInteger(o) || o < 0 || o > 255)) return false;
11
+ const [a, b] = octets;
12
+ if (a === 127 || a === 10 || a === 0) return true; // loopback / private / "this host"
13
+ if (a === 169 && b === 254) return true; // link-local (incl. cloud metadata)
14
+ if (a === 192 && b === 168) return true; // private
15
+ if (a === 172 && b >= 16 && b <= 31) return true; // private
16
+ return false;
17
+ };
18
+
19
+ export const isBlockedHost = (hostname: string): boolean => {
20
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, '');
21
+ if (!host) return true;
22
+ if (host === 'localhost' || host.endsWith('.localhost')) return true;
23
+ if (host === '::1' || host === '::') return true;
24
+ // IPv6 link-local (fe80::/10) and unique-local (fc00::/7).
25
+ if (
26
+ host.startsWith('fe80:') ||
27
+ host.startsWith('fc') ||
28
+ host.startsWith('fd')
29
+ ) {
30
+ return true;
31
+ }
32
+ if (ipv4InPrivateRange(host)) return true;
33
+ return false;
34
+ };
@@ -0,0 +1,20 @@
1
+ import { constants } from './constants';
2
+
3
+ export interface UsageTally {
4
+ tool: number;
5
+ prompt: number;
6
+ resource: number;
7
+ }
8
+
9
+ // Count a batch of invocations by usage kind. Shared by the channel runner and
10
+ // the MCP flush so both feed the artifact's denormalized usage totals the same
11
+ // way (anything that isn't tool/prompt/resource — protocol noise — is ignored).
12
+ export const tallyUsageKinds = (items: { kind: string }[]): UsageTally => {
13
+ const tally: UsageTally = { tool: 0, prompt: 0, resource: 0 };
14
+ for (const { kind } of items) {
15
+ if (kind === constants.USAGE_KIND_TOOL) tally.tool += 1;
16
+ else if (kind === constants.USAGE_KIND_PROMPT) tally.prompt += 1;
17
+ else if (kind === constants.USAGE_KIND_RESOURCE) tally.resource += 1;
18
+ }
19
+ return tally;
20
+ };
@@ -0,0 +1,39 @@
1
+ // Wire protocol shared between the worker (which constructs the request) and
2
+ // the resource-handler container (which parses it and calls the Telegram Bot
3
+ // API). Worker sends a multipart/form-data POST with one `metadata` field
4
+ // (JSON matching TelegramSendRequest) and exactly one `file` field (binary).
5
+ //
6
+ // Telegram supports one media object per send call, so unlike Gmail this is
7
+ // always a single attachment.
8
+
9
+ export interface TelegramSendRequest {
10
+ botToken: string;
11
+ chatId: number;
12
+ replyToMessageId?: number;
13
+ caption?: string;
14
+ parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2';
15
+ }
16
+
17
+ export interface TelegramSendResponse {
18
+ ok: boolean;
19
+ result?: { message_id: number };
20
+ description?: string;
21
+ }
22
+
23
+ // Wire protocol for sending a PROXIED (remote MCP) resource as a file. Unlike
24
+ // TelegramSendRequest (where the worker has already read the bytes and posts
25
+ // them as multipart), here the worker sends only the connection details as
26
+ // JSON and the resource-handler container does the remote read + decode + send
27
+ // itself — so a large file's bytes never transit the 128 MiB worker. The worker
28
+ // resolves the (small) auth header; the file itself stays in the container.
29
+ export interface TelegramSendRemoteResourceRequest {
30
+ telegram: TelegramSendRequest;
31
+ remote: {
32
+ url: string;
33
+ transport: string;
34
+ // Single header injected on the remote MCP connection (e.g. Authorization).
35
+ authHeader?: { name: string; value: string } | null;
36
+ uri: string;
37
+ timeoutMs: number;
38
+ };
39
+ }