@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.
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +23 -0
- package/dist/apiError.d.ts +3 -0
- package/dist/apiError.d.ts.map +1 -0
- package/dist/apiError.js +36 -0
- package/dist/attachment.d.ts +33 -0
- package/dist/attachment.d.ts.map +1 -0
- package/dist/attachment.js +59 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.d.ts.map +1 -0
- package/dist/base64.js +50 -0
- package/dist/channelDebounce.d.ts +26 -0
- package/dist/channelDebounce.d.ts.map +1 -0
- package/dist/channelDebounce.js +35 -0
- package/dist/channelNotifier.d.ts +9 -0
- package/dist/channelNotifier.d.ts.map +1 -0
- package/dist/channelNotifier.js +31 -0
- package/dist/chunking.d.ts +28 -0
- package/dist/chunking.d.ts.map +1 -0
- package/dist/chunking.js +97 -0
- package/dist/cliConstants.d.ts +9 -0
- package/dist/cliConstants.d.ts.map +1 -0
- package/dist/cliConstants.js +47 -0
- package/dist/coerce.d.ts +2 -0
- package/dist/coerce.d.ts.map +1 -0
- package/dist/coerce.js +13 -0
- package/dist/constants.d.ts +624 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2220 -0
- package/dist/crypto.d.ts +14 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +93 -0
- package/dist/customCodeProject.d.ts +91 -0
- package/dist/customCodeProject.d.ts.map +1 -0
- package/dist/customCodeProject.js +154 -0
- package/dist/customCodeToken.d.ts +41 -0
- package/dist/customCodeToken.d.ts.map +1 -0
- package/dist/customCodeToken.js +115 -0
- package/dist/deadline.d.ts +6 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +45 -0
- package/dist/discordSend.d.ts +25 -0
- package/dist/discordSend.d.ts.map +1 -0
- package/dist/discordSend.js +8 -0
- package/dist/embeddable.d.ts +2 -0
- package/dist/embeddable.d.ts.map +1 -0
- package/dist/embeddable.js +6 -0
- package/dist/exposedResource.d.ts +6 -0
- package/dist/exposedResource.d.ts.map +1 -0
- package/dist/exposedResource.js +18 -0
- package/dist/extractedDocument.d.ts +70 -0
- package/dist/extractedDocument.d.ts.map +1 -0
- package/dist/extractedDocument.js +2 -0
- package/dist/fetcher.d.ts +8 -0
- package/dist/fetcher.d.ts.map +1 -0
- package/dist/fetcher.js +68 -0
- package/dist/formatFilename.d.ts +2 -0
- package/dist/formatFilename.d.ts.map +1 -0
- package/dist/formatFilename.js +19 -0
- package/dist/formatRelative.d.ts +12 -0
- package/dist/formatRelative.d.ts.map +1 -0
- package/dist/formatRelative.js +28 -0
- package/dist/formatRelativeTime.d.ts +2 -0
- package/dist/formatRelativeTime.d.ts.map +1 -0
- package/dist/formatRelativeTime.js +49 -0
- package/dist/getEnv.d.ts +5 -0
- package/dist/getEnv.d.ts.map +1 -0
- package/dist/getEnv.js +11 -0
- package/dist/gmailSend.d.ts +24 -0
- package/dist/gmailSend.d.ts.map +1 -0
- package/dist/gmailSend.js +9 -0
- package/dist/index.d.ts +2839 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -0
- package/dist/jsonSchemaToZodShape.d.ts +38 -0
- package/dist/jsonSchemaToZodShape.d.ts.map +1 -0
- package/dist/jsonSchemaToZodShape.js +117 -0
- package/dist/l2Normalize.d.ts +2 -0
- package/dist/l2Normalize.d.ts.map +1 -0
- package/dist/l2Normalize.js +25 -0
- package/dist/languageCookieDomain.d.ts +21 -0
- package/dist/languageCookieDomain.d.ts.map +1 -0
- package/dist/languageCookieDomain.js +39 -0
- package/dist/localizeZodIssue.d.ts +43 -0
- package/dist/localizeZodIssue.d.ts.map +1 -0
- package/dist/localizeZodIssue.js +90 -0
- package/dist/mailHeaders.d.ts +4 -0
- package/dist/mailHeaders.d.ts.map +1 -0
- package/dist/mailHeaders.js +11 -0
- package/dist/mcpProxy.d.ts +2 -0
- package/dist/mcpProxy.d.ts.map +1 -0
- package/dist/mcpProxy.js +21 -0
- package/dist/mimeMessage.d.ts +21 -0
- package/dist/mimeMessage.d.ts.map +1 -0
- package/dist/mimeMessage.js +58 -0
- package/dist/oauth.d.ts +21 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +75 -0
- package/dist/oauthProviders.d.ts +12 -0
- package/dist/oauthProviders.d.ts.map +1 -0
- package/dist/oauthProviders.js +99 -0
- package/dist/outlookSend.d.ts +19 -0
- package/dist/outlookSend.d.ts.map +1 -0
- package/dist/outlookSend.js +9 -0
- package/dist/parseHttpError.d.ts +2 -0
- package/dist/parseHttpError.d.ts.map +1 -0
- package/dist/parseHttpError.js +23 -0
- package/dist/planLimitError.d.ts +18 -0
- package/dist/planLimitError.d.ts.map +1 -0
- package/dist/planLimitError.js +35 -0
- package/dist/processQueueBatch.d.ts +17 -0
- package/dist/processQueueBatch.d.ts.map +1 -0
- package/dist/processQueueBatch.js +32 -0
- package/dist/reservedToolName.d.ts +16 -0
- package/dist/reservedToolName.d.ts.map +1 -0
- package/dist/reservedToolName.js +25 -0
- package/dist/resourceUri.d.ts +2 -0
- package/dist/resourceUri.d.ts.map +1 -0
- package/dist/resourceUri.js +19 -0
- package/dist/retry.d.ts +10 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +37 -0
- package/dist/sanitize.d.ts +7 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +69 -0
- package/dist/schema.d.ts +2175 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +1382 -0
- package/dist/sdkConstants.d.ts +15 -0
- package/dist/sdkConstants.d.ts.map +1 -0
- package/dist/sdkConstants.js +43 -0
- package/dist/slackSend.d.ts +38 -0
- package/dist/slackSend.d.ts.map +1 -0
- package/dist/slackSend.js +14 -0
- package/dist/slug.d.ts +5 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +34 -0
- package/dist/slugifyTitle.d.ts +2 -0
- package/dist/slugifyTitle.d.ts.map +1 -0
- package/dist/slugifyTitle.js +11 -0
- package/dist/sources.d.ts +43 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/sources.js +73 -0
- package/dist/ssrf.d.ts +3 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +46 -0
- package/dist/tallyUsageKinds.d.ts +9 -0
- package/dist/tallyUsageKinds.d.ts.map +1 -0
- package/dist/tallyUsageKinds.js +20 -0
- package/dist/telegramSend.d.ts +28 -0
- package/dist/telegramSend.d.ts.map +1 -0
- package/dist/telegramSend.js +9 -0
- package/dist/toolCatalog.d.ts +452 -0
- package/dist/toolCatalog.d.ts.map +1 -0
- package/dist/toolCatalog.js +552 -0
- package/dist/validateMessageVariables.d.ts +7 -0
- package/dist/validateMessageVariables.d.ts.map +1 -0
- package/dist/validateMessageVariables.js +16 -0
- package/dist/vendorError.d.ts +2 -0
- package/dist/vendorError.d.ts.map +1 -0
- package/dist/vendorError.js +25 -0
- package/dist/whatsappSend.d.ts +31 -0
- package/dist/whatsappSend.d.ts.map +1 -0
- package/dist/whatsappSend.js +11 -0
- package/package.json +76 -0
- package/src/apiError.ts +40 -0
- package/src/attachment.ts +97 -0
- package/src/base64.ts +60 -0
- package/src/channelDebounce.ts +76 -0
- package/src/channelNotifier.ts +39 -0
- package/src/chunking.ts +131 -0
- package/src/cliConstants.ts +50 -0
- package/src/coerce.ts +9 -0
- package/src/constants.ts +2542 -0
- package/src/crypto.ts +108 -0
- package/src/customCodeProject.ts +182 -0
- package/src/customCodeToken.ts +198 -0
- package/src/deadline.ts +43 -0
- package/src/discordSend.ts +41 -0
- package/src/embeddable.ts +4 -0
- package/src/exposedResource.ts +40 -0
- package/src/extractedDocument.ts +62 -0
- package/src/fetcher.ts +82 -0
- package/src/formatFilename.ts +19 -0
- package/src/formatRelative.ts +36 -0
- package/src/formatRelativeTime.ts +45 -0
- package/src/getEnv.ts +13 -0
- package/src/gmailSend.ts +34 -0
- package/src/index.ts +397 -0
- package/src/jsonSchemaToZodShape.ts +111 -0
- package/src/l2Normalize.ts +19 -0
- package/src/languageCookieDomain.ts +37 -0
- package/src/localizeZodIssue.ts +124 -0
- package/src/mailHeaders.ts +12 -0
- package/src/mcpProxy.ts +20 -0
- package/src/mimeMessage.ts +82 -0
- package/src/oauth.ts +103 -0
- package/src/oauthProviders.ts +118 -0
- package/src/outlookSend.ts +44 -0
- package/src/parseHttpError.ts +16 -0
- package/src/planLimitError.ts +50 -0
- package/src/processQueueBatch.ts +46 -0
- package/src/reservedToolName.ts +27 -0
- package/src/resourceUri.ts +17 -0
- package/src/retry.ts +43 -0
- package/src/sanitize.ts +67 -0
- package/src/schema.ts +1624 -0
- package/src/sdkConstants.ts +44 -0
- package/src/slackSend.ts +65 -0
- package/src/slug.ts +37 -0
- package/src/slugifyTitle.ts +8 -0
- package/src/sources.ts +117 -0
- package/src/ssrf.ts +34 -0
- package/src/tallyUsageKinds.ts +20 -0
- package/src/telegramSend.ts +39 -0
- package/src/toolCatalog.ts +635 -0
- package/src/validateMessageVariables.ts +18 -0
- package/src/vendorError.ts +19 -0
- 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;
|
package/src/slackSend.ts
ADDED
|
@@ -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
|
+
}
|