@larkup/workspace-server 0.0.0-stage → 0.2.0
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 +176 -0
- package/README.md +2 -2
- package/dist-types/app/api/chat/feedback/route.d.ts +4 -0
- package/dist-types/app/api/chat/feedback/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/route.d.ts +3 -0
- package/dist-types/app/api/chat/route.d.ts.map +1 -0
- package/dist-types/app/api/chat/tools.d.ts +67 -0
- package/dist-types/app/api/chat/tools.d.ts.map +1 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts +3 -0
- package/dist-types/app/api/config/test-embedding/route.d.ts.map +1 -0
- package/dist-types/app/api/config/verify/route.d.ts +3 -0
- package/dist-types/app/api/config/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/describe-image/route.d.ts +4 -0
- package/dist-types/app/api/describe-image/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/edit/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts +9 -0
- package/dist-types/app/api/doc-editor/export/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts +10 -0
- package/dist-types/app/api/doc-editor/parse/route.d.ts.map +1 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts +2 -0
- package/dist-types/app/api/doc-editor/restore/route.d.ts.map +1 -0
- package/dist-types/app/api/document/sign/route.d.ts +2 -0
- package/dist-types/app/api/document/sign/route.d.ts.map +1 -0
- package/dist-types/app/api/files/remote/route.d.ts +5 -0
- package/dist-types/app/api/files/remote/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts +9 -0
- package/dist-types/app/api/mcp/[connectionId]/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts +8 -0
- package/dist-types/app/api/mcp/[connectionId]/test/route.d.ts.map +1 -0
- package/dist-types/app/api/mcp/route.d.ts +4 -0
- package/dist-types/app/api/mcp/route.d.ts.map +1 -0
- package/dist-types/app/api/parse-file/route.d.ts +6 -0
- package/dist-types/app/api/parse-file/route.d.ts.map +1 -0
- package/dist-types/app/api/pdf-page/route.d.ts +4 -0
- package/dist-types/app/api/pdf-page/route.d.ts.map +1 -0
- package/dist-types/app/api/proxy/verify/route.d.ts +4 -0
- package/dist-types/app/api/proxy/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/search/bing/route.d.ts +5 -0
- package/dist-types/app/api/search/bing/route.d.ts.map +1 -0
- package/dist-types/app/api/search/brave/route.d.ts +5 -0
- package/dist-types/app/api/search/brave/route.d.ts.map +1 -0
- package/dist-types/app/api/search/exa/route.d.ts +5 -0
- package/dist-types/app/api/search/exa/route.d.ts.map +1 -0
- package/dist-types/app/api/search/google/route.d.ts +30 -0
- package/dist-types/app/api/search/google/route.d.ts.map +1 -0
- package/dist-types/app/api/search/route.d.ts +7 -0
- package/dist-types/app/api/search/route.d.ts.map +1 -0
- package/dist-types/app/api/search/tavily/route.d.ts +5 -0
- package/dist-types/app/api/search/tavily/route.d.ts.map +1 -0
- package/dist-types/app/api/search/verify/route.d.ts +4 -0
- package/dist-types/app/api/search/verify/route.d.ts.map +1 -0
- package/dist-types/app/api/tabular/route.d.ts +9 -0
- package/dist-types/app/api/tabular/route.d.ts.map +1 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts +10 -0
- package/dist-types/lib/chat/assistant-source-scope.d.ts.map +1 -0
- package/dist-types/lib/chat/conversation-memory.d.ts +80 -0
- package/dist-types/lib/chat/conversation-memory.d.ts.map +1 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts +7 -0
- package/dist-types/lib/chat/gateway-fallbacks.d.ts.map +1 -0
- package/dist-types/lib/chat/image-description.d.ts +10 -0
- package/dist-types/lib/chat/image-description.d.ts.map +1 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts +14 -0
- package/dist-types/lib/chat/media-retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/media-source-routing.d.ts +53 -0
- package/dist-types/lib/chat/media-source-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/message-input.d.ts +8 -0
- package/dist-types/lib/chat/message-input.d.ts.map +1 -0
- package/dist-types/lib/chat/model-provider.d.ts +23 -0
- package/dist-types/lib/chat/model-provider.d.ts.map +1 -0
- package/dist-types/lib/chat/response-style.d.ts +2 -0
- package/dist-types/lib/chat/response-style.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts +32 -0
- package/dist-types/lib/chat/retrieval-ranking.d.ts.map +1 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts +39 -0
- package/dist-types/lib/chat/retrieval-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts +15 -0
- package/dist-types/lib/chat/tabular-dataset-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts +12 -0
- package/dist-types/lib/chat/tabular-query-plan.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-routing.d.ts +41 -0
- package/dist-types/lib/chat/tabular-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts +33 -0
- package/dist-types/lib/chat/tabular-sandbox-files.d.ts.map +1 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts +31 -0
- package/dist-types/lib/chat/tabular-visualization.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts +11 -0
- package/dist-types/lib/chat/tool-call-repair.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-context.d.ts +72 -0
- package/dist-types/lib/chat/tool-context.d.ts.map +1 -0
- package/dist-types/lib/chat/tool-registry.d.ts +3 -0
- package/dist-types/lib/chat/tool-registry.d.ts.map +1 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts +33 -0
- package/dist-types/lib/chat/video-rag-routing.d.ts.map +1 -0
- package/dist-types/lib/chat/visual-routing.d.ts +53 -0
- package/dist-types/lib/chat/visual-routing.d.ts.map +1 -0
- package/dist-types/lib/enterprise-client.d.ts +31 -0
- package/dist-types/lib/enterprise-client.d.ts.map +1 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts +7 -0
- package/dist-types/lib/marketplace/tool-runtime-config.d.ts.map +1 -0
- package/dist-types/lib/media/knowledge.d.ts +47 -0
- package/dist-types/lib/media/knowledge.d.ts.map +1 -0
- package/dist-types/lib/media/pdf-inspection.d.ts +30 -0
- package/dist-types/lib/media/pdf-inspection.d.ts.map +1 -0
- package/dist-types/lib/media/source-identity.d.ts +12 -0
- package/dist-types/lib/media/source-identity.d.ts.map +1 -0
- package/dist-types/lib/media/source-utils.d.ts +157 -0
- package/dist-types/lib/media/source-utils.d.ts.map +1 -0
- package/dist-types/lib/media/video/quick-look.d.ts +60 -0
- package/dist-types/lib/media/video/quick-look.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts +521 -0
- package/dist-types/lib/media/video-intelligence-adapter.d.ts.map +1 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts +32 -0
- package/dist-types/lib/media/video-intelligence-connection.d.ts.map +1 -0
- package/dist-types/lib/remote-files.d.ts +12 -0
- package/dist-types/lib/remote-files.d.ts.map +1 -0
- package/dist-types/lib/search/provider-verification.d.ts +5 -0
- package/dist-types/lib/search/provider-verification.d.ts.map +1 -0
- package/dist-types/runtime-host.d.ts +20 -0
- package/dist-types/runtime-host.d.ts.map +1 -0
- package/package.json +338 -3
- package/src/app/api/chat/feedback/route.ts +77 -0
- package/src/app/api/chat/route.ts +2277 -0
- package/src/app/api/chat/tools.ts +3563 -0
- package/src/app/api/config/test-embedding/route.ts +39 -0
- package/src/app/api/config/verify/route.ts +309 -0
- package/src/app/api/describe-image/route.ts +108 -0
- package/src/app/api/doc-editor/edit/route.ts +87 -0
- package/src/app/api/doc-editor/export/route.ts +32 -0
- package/src/app/api/doc-editor/parse/route.ts +74 -0
- package/src/app/api/doc-editor/restore/route.ts +70 -0
- package/src/app/api/document/sign/route.ts +55 -0
- package/src/app/api/files/remote/route.ts +32 -0
- package/src/app/api/mcp/[connectionId]/route.ts +29 -0
- package/src/app/api/mcp/[connectionId]/test/route.ts +13 -0
- package/src/app/api/mcp/route.ts +22 -0
- package/src/app/api/parse-file/route.ts +86 -0
- package/src/app/api/pdf-page/route.ts +40 -0
- package/src/app/api/proxy/verify/route.ts +78 -0
- package/src/app/api/search/bing/route.ts +86 -0
- package/src/app/api/search/brave/route.ts +88 -0
- package/src/app/api/search/exa/route.ts +100 -0
- package/src/app/api/search/google/route.ts +147 -0
- package/src/app/api/search/route.ts +28 -0
- package/src/app/api/search/tavily/route.ts +93 -0
- package/src/app/api/search/verify/route.ts +27 -0
- package/src/app/api/tabular/route.ts +68 -0
- package/src/lib/chat/assistant-source-scope.ts +32 -0
- package/src/lib/chat/conversation-memory.ts +621 -0
- package/src/lib/chat/gateway-fallbacks.ts +12 -0
- package/src/lib/chat/image-description.ts +21 -0
- package/src/lib/chat/media-retrieval-routing.ts +37 -0
- package/src/lib/chat/media-source-routing.ts +100 -0
- package/src/lib/chat/message-input.ts +48 -0
- package/src/lib/chat/model-provider.ts +157 -0
- package/src/lib/chat/response-style.ts +3 -0
- package/src/lib/chat/retrieval-ranking.ts +126 -0
- package/src/lib/chat/retrieval-routing.ts +221 -0
- package/src/lib/chat/tabular-dataset-routing.ts +93 -0
- package/src/lib/chat/tabular-query-plan.ts +195 -0
- package/src/lib/chat/tabular-routing.ts +122 -0
- package/src/lib/chat/tabular-sandbox-files.ts +78 -0
- package/src/lib/chat/tabular-visualization.ts +165 -0
- package/src/lib/chat/tool-call-repair.ts +84 -0
- package/src/lib/chat/tool-context.ts +1222 -0
- package/src/lib/chat/tool-registry.ts +6 -0
- package/src/lib/chat/video-rag-routing.ts +149 -0
- package/src/lib/chat/visual-routing.ts +225 -0
- package/src/lib/enterprise-client.ts +96 -0
- package/src/lib/marketplace/tool-runtime-config.ts +155 -0
- package/src/lib/media/knowledge.ts +489 -0
- package/src/lib/media/pdf-inspection.ts +280 -0
- package/src/lib/media/source-identity.ts +50 -0
- package/src/lib/media/source-utils.ts +1156 -0
- package/src/lib/media/video/quick-look.ts +368 -0
- package/src/lib/media/video-intelligence-adapter.ts +1946 -0
- package/src/lib/media/video-intelligence-connection.ts +151 -0
- package/src/lib/remote-files.ts +182 -0
- package/src/lib/search/provider-verification.ts +95 -0
- package/src/runtime-host.ts +31 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readConfig, writeConfig } from '@larkup/core/config-store';
|
|
3
|
+
import { withGlobalVisionGatewayConfig } from '../marketplace/tool-runtime-config';
|
|
4
|
+
|
|
5
|
+
type ProvisioningClient = {
|
|
6
|
+
getUsage?: () => Promise<unknown>;
|
|
7
|
+
provisionDeviceAccess?: (installationId: string) => Promise<{
|
|
8
|
+
apiKey: string;
|
|
9
|
+
entitlement: {
|
|
10
|
+
plan: string;
|
|
11
|
+
sourceMinutesPerMonth: number | null;
|
|
12
|
+
maxConcurrentJobs: number;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type VideoExtension = {
|
|
18
|
+
createClient(context: {
|
|
19
|
+
config: Record<string, unknown>;
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
}): ProvisioningClient;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a per-installation cloud credential once, then keeps it in this
|
|
26
|
+
* project's local config. The cloud stores hashes of both identifiers only.
|
|
27
|
+
*/
|
|
28
|
+
export async function resolveVideoIntelligenceConnection(
|
|
29
|
+
extension: VideoExtension,
|
|
30
|
+
installedConfig: Record<string, unknown>,
|
|
31
|
+
): Promise<{
|
|
32
|
+
config: Record<string, unknown>;
|
|
33
|
+
provisioned: boolean;
|
|
34
|
+
entitlement?: {
|
|
35
|
+
plan: string;
|
|
36
|
+
sourceMinutesPerMonth: number | null;
|
|
37
|
+
maxConcurrentJobs: number;
|
|
38
|
+
};
|
|
39
|
+
}> {
|
|
40
|
+
const globalConfig = await readConfig();
|
|
41
|
+
const current = withGlobalVisionGatewayConfig(
|
|
42
|
+
{
|
|
43
|
+
...installedConfig,
|
|
44
|
+
...(globalConfig.toolConfigs?.['video-intelligence'] ?? {}),
|
|
45
|
+
},
|
|
46
|
+
globalConfig,
|
|
47
|
+
);
|
|
48
|
+
const forceProvision = current.forceProvisionManagedCloud === true;
|
|
49
|
+
const { forceProvisionManagedCloud: _forceProvisionManagedCloud, ...connectionConfig } = current;
|
|
50
|
+
if (connectionConfig.runtimeMode && connectionConfig.runtimeMode !== 'managed-cloud') {
|
|
51
|
+
return { config: connectionConfig, provisioned: false };
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
!forceProvision &&
|
|
55
|
+
typeof connectionConfig.cloudAccessKey === 'string' &&
|
|
56
|
+
connectionConfig.cloudAccessKey.trim()
|
|
57
|
+
) {
|
|
58
|
+
const existingClient = extension.createClient({
|
|
59
|
+
config: connectionConfig,
|
|
60
|
+
fetch: globalThis.fetch,
|
|
61
|
+
});
|
|
62
|
+
if (typeof existingClient.getUsage !== 'function') {
|
|
63
|
+
return { config: connectionConfig, provisioned: false };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await existingClient.getUsage();
|
|
67
|
+
return { config: connectionConfig, provisioned: false };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
if (!/invalid api key|unauthorized|unauthorised/i.test(message)) throw error;
|
|
71
|
+
// A managed deployment can move while an installation still has a key
|
|
72
|
+
// issued by the previous control plane. Re-provision the same opaque
|
|
73
|
+
// installation ID once instead of letting every later job fail at submit.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const installationId =
|
|
78
|
+
typeof connectionConfig.cloudInstallationId === 'string' &&
|
|
79
|
+
connectionConfig.cloudInstallationId.length >= 32
|
|
80
|
+
? connectionConfig.cloudInstallationId
|
|
81
|
+
: randomUUID();
|
|
82
|
+
const client = extension.createClient({
|
|
83
|
+
config: { ...connectionConfig, cloudInstallationId: installationId },
|
|
84
|
+
fetch: globalThis.fetch,
|
|
85
|
+
});
|
|
86
|
+
if (typeof client.provisionDeviceAccess !== 'function') {
|
|
87
|
+
// An already-installed pre-managed version can still be cached in the
|
|
88
|
+
// Marketplace manifest. Provision through the current first-party client
|
|
89
|
+
// so a normal re-index self-heals the connection instead of asking the
|
|
90
|
+
// user to reinstall a tool or reveal an endpoint/key.
|
|
91
|
+
const endpoint =
|
|
92
|
+
process.env.LARKUP_VIDEO_INTELLIGENCE_CLOUD_ENDPOINT ??
|
|
93
|
+
'https://7w1bab08jf.execute-api.eu-central-1.amazonaws.com';
|
|
94
|
+
const response = await globalThis.fetch(`${endpoint}/v1/device-keys`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'Content-Type': 'application/json' },
|
|
97
|
+
body: JSON.stringify({ installationId }),
|
|
98
|
+
});
|
|
99
|
+
const provisioned = (await response.json().catch(() => ({}))) as {
|
|
100
|
+
apiKey?: string;
|
|
101
|
+
entitlement?: {
|
|
102
|
+
plan: string;
|
|
103
|
+
sourceMinutesPerMonth: number | null;
|
|
104
|
+
maxConcurrentJobs: number;
|
|
105
|
+
};
|
|
106
|
+
error?: string;
|
|
107
|
+
};
|
|
108
|
+
if (!response.ok || !provisioned.apiKey || !provisioned.entitlement) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
provisioned.error || 'Could not create the managed Video Intelligence connection.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return persistManagedConnection(globalConfig, connectionConfig, installationId, {
|
|
114
|
+
apiKey: provisioned.apiKey,
|
|
115
|
+
entitlement: provisioned.entitlement,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const provisioned = await client.provisionDeviceAccess(installationId);
|
|
119
|
+
return persistManagedConnection(globalConfig, connectionConfig, installationId, provisioned);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function persistManagedConnection(
|
|
123
|
+
globalConfig: Awaited<ReturnType<typeof readConfig>>,
|
|
124
|
+
current: Record<string, unknown>,
|
|
125
|
+
installationId: string,
|
|
126
|
+
provisioned: {
|
|
127
|
+
apiKey: string;
|
|
128
|
+
entitlement: {
|
|
129
|
+
plan: string;
|
|
130
|
+
sourceMinutesPerMonth: number | null;
|
|
131
|
+
maxConcurrentJobs: number;
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
) {
|
|
135
|
+
const { forceProvisionManagedCloud: _forceProvisionManagedCloud, ...existingToolConfig } =
|
|
136
|
+
globalConfig.toolConfigs?.['video-intelligence'] ?? {};
|
|
137
|
+
const toolConfigs = {
|
|
138
|
+
...(globalConfig.toolConfigs ?? {}),
|
|
139
|
+
'video-intelligence': {
|
|
140
|
+
...existingToolConfig,
|
|
141
|
+
cloudInstallationId: installationId,
|
|
142
|
+
cloudAccessKey: provisioned.apiKey,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
await writeConfig({ ...globalConfig, toolConfigs });
|
|
146
|
+
return {
|
|
147
|
+
config: { ...current, ...toolConfigs['video-intelligence'] },
|
|
148
|
+
provisioned: true,
|
|
149
|
+
entitlement: provisioned.entitlement,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
|
|
4
|
+
export const MAX_REMOTE_FILE_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
7
|
+
'txt',
|
|
8
|
+
'md',
|
|
9
|
+
'markdown',
|
|
10
|
+
'toml',
|
|
11
|
+
'json',
|
|
12
|
+
'csv',
|
|
13
|
+
'html',
|
|
14
|
+
'htm',
|
|
15
|
+
'log',
|
|
16
|
+
'xlsx',
|
|
17
|
+
'xls',
|
|
18
|
+
'pdf',
|
|
19
|
+
'doc',
|
|
20
|
+
'docx',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const EXTENSION_BY_MIME: Record<string, string> = {
|
|
24
|
+
'application/pdf': 'pdf',
|
|
25
|
+
'application/json': 'json',
|
|
26
|
+
'text/csv': 'csv',
|
|
27
|
+
'text/markdown': 'md',
|
|
28
|
+
'text/plain': 'txt',
|
|
29
|
+
'text/html': 'html',
|
|
30
|
+
'application/vnd.ms-excel': 'xls',
|
|
31
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
|
|
32
|
+
'application/msword': 'doc',
|
|
33
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface RemoteFileDownload {
|
|
37
|
+
bytes: Buffer;
|
|
38
|
+
contentType: string;
|
|
39
|
+
fileName: string;
|
|
40
|
+
sourceUrl: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeRemoteFileUrl(value: string): URL {
|
|
44
|
+
const url = new URL(value.trim());
|
|
45
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
46
|
+
throw new Error('Only public http(s) file URLs are supported.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Make share links work without asking people to find a provider-specific
|
|
50
|
+
// "raw" download URL themselves.
|
|
51
|
+
if (url.hostname === 'github.com') {
|
|
52
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
53
|
+
if (parts.length >= 5 && parts[2] === 'blob') {
|
|
54
|
+
url.hostname = 'raw.githubusercontent.com';
|
|
55
|
+
url.pathname = `/${parts[0]}/${parts[1]}/${parts.slice(3).join('/')}`;
|
|
56
|
+
url.search = '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (url.hostname === 'huggingface.co' && url.pathname.includes('/blob/')) {
|
|
60
|
+
url.pathname = url.pathname.replace('/blob/', '/resolve/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isPrivateAddress(address: string) {
|
|
67
|
+
const normalized = address.toLowerCase();
|
|
68
|
+
if (
|
|
69
|
+
normalized === '::1' ||
|
|
70
|
+
normalized === '::' ||
|
|
71
|
+
normalized.startsWith('fe80:') ||
|
|
72
|
+
normalized.startsWith('fc') ||
|
|
73
|
+
normalized.startsWith('fd')
|
|
74
|
+
) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const parts = (normalized.startsWith('::ffff:') ? normalized.slice(7) : normalized)
|
|
78
|
+
.split('.')
|
|
79
|
+
.map(Number);
|
|
80
|
+
if (parts.length !== 4 || parts.some(Number.isNaN)) return false;
|
|
81
|
+
const [a, b] = parts;
|
|
82
|
+
return (
|
|
83
|
+
a === 0 ||
|
|
84
|
+
a === 10 ||
|
|
85
|
+
a === 127 ||
|
|
86
|
+
(a === 169 && b === 254) ||
|
|
87
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
88
|
+
(a === 192 && b === 168) ||
|
|
89
|
+
a >= 224
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function assertPublicHost(hostname: string) {
|
|
94
|
+
const addresses = isIP(hostname)
|
|
95
|
+
? [{ address: hostname }]
|
|
96
|
+
: await lookup(hostname, { all: true, verbatim: true });
|
|
97
|
+
if (!addresses.length || addresses.some(({ address }) => isPrivateAddress(address))) {
|
|
98
|
+
throw new Error('Private or local file URLs are not supported.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getRemoteFileName(response: Response, sourceUrl: URL, contentType: string) {
|
|
103
|
+
const disposition = response.headers.get('content-disposition') ?? '';
|
|
104
|
+
const encodedMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
|
|
105
|
+
const plainMatch = disposition.match(/filename\s*=\s*"?([^";]+)"?/i)?.[1];
|
|
106
|
+
let name = encodedMatch ? decodeURIComponent(encodedMatch) : plainMatch;
|
|
107
|
+
|
|
108
|
+
if (!name) {
|
|
109
|
+
name = decodeURIComponent(sourceUrl.pathname.split('/').filter(Boolean).pop() || 'remote-file');
|
|
110
|
+
}
|
|
111
|
+
name = name.replace(/[\\/\0\r\n]/g, '-').trim() || 'remote-file';
|
|
112
|
+
|
|
113
|
+
const extension = name.split('.').pop()?.toLowerCase();
|
|
114
|
+
// GitHub and similar providers return source files as text/plain. Retain an
|
|
115
|
+
// unfamiliar extension (for example .py) instead of relabeling it as .txt.
|
|
116
|
+
if (extension && (SUPPORTED_EXTENSIONS.has(extension) || contentType.startsWith('text/'))) {
|
|
117
|
+
return name;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const inferredExtension = EXTENSION_BY_MIME[contentType];
|
|
121
|
+
if (inferredExtension) return `${name.replace(/\.[^.]+$/, '')}.${inferredExtension}`;
|
|
122
|
+
throw new Error(
|
|
123
|
+
'This URL does not point to a supported document. Use PDF, Word, CSV, JSON, Excel, or text.',
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function readBoundedBody(response: Response) {
|
|
128
|
+
const declaredSize = Number(response.headers.get('content-length'));
|
|
129
|
+
if (Number.isFinite(declaredSize) && declaredSize > MAX_REMOTE_FILE_BYTES) {
|
|
130
|
+
throw new Error('Remote files must be 50 MB or smaller.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!response.body) return Buffer.from(await response.arrayBuffer());
|
|
134
|
+
const reader = response.body.getReader();
|
|
135
|
+
const chunks: Uint8Array[] = [];
|
|
136
|
+
let size = 0;
|
|
137
|
+
try {
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
if (done) break;
|
|
141
|
+
size += value.byteLength;
|
|
142
|
+
if (size > MAX_REMOTE_FILE_BYTES) {
|
|
143
|
+
await reader.cancel();
|
|
144
|
+
throw new Error('Remote files must be 50 MB or smaller.');
|
|
145
|
+
}
|
|
146
|
+
chunks.push(value);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
reader.releaseLock();
|
|
150
|
+
}
|
|
151
|
+
return Buffer.concat(chunks);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function downloadRemoteFile(value: string): Promise<RemoteFileDownload> {
|
|
155
|
+
let current = normalizeRemoteFileUrl(value);
|
|
156
|
+
for (let redirects = 0; redirects <= 5; redirects++) {
|
|
157
|
+
await assertPublicHost(current.hostname);
|
|
158
|
+
const response = await fetch(current, {
|
|
159
|
+
redirect: 'manual',
|
|
160
|
+
headers: { Accept: 'application/pdf,text/*,application/json,application/octet-stream,*/*' },
|
|
161
|
+
});
|
|
162
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
163
|
+
const location = response.headers.get('location');
|
|
164
|
+
if (!location) throw new Error('The file URL redirected without a destination.');
|
|
165
|
+
current = normalizeRemoteFileUrl(new URL(location, current).toString());
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!response.ok) throw new Error(`Could not download the file (HTTP ${response.status}).`);
|
|
169
|
+
|
|
170
|
+
const contentType = (response.headers.get('content-type') ?? 'application/octet-stream')
|
|
171
|
+
.split(';')[0]
|
|
172
|
+
.trim()
|
|
173
|
+
.toLowerCase();
|
|
174
|
+
return {
|
|
175
|
+
bytes: await readBoundedBody(response),
|
|
176
|
+
contentType,
|
|
177
|
+
fileName: getRemoteFileName(response, current, contentType),
|
|
178
|
+
sourceUrl: current.toString(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
throw new Error('The file URL redirected too many times.');
|
|
182
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const SEARCH_VERIFICATION_PROVIDERS = [
|
|
2
|
+
'serper',
|
|
3
|
+
'google',
|
|
4
|
+
'tavily',
|
|
5
|
+
'brave',
|
|
6
|
+
'bing',
|
|
7
|
+
'exa',
|
|
8
|
+
'firecrawl',
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type SearchVerificationProvider = (typeof SEARCH_VERIFICATION_PROVIDERS)[number];
|
|
12
|
+
|
|
13
|
+
export function isSearchVerificationProvider(
|
|
14
|
+
provider: string,
|
|
15
|
+
): provider is SearchVerificationProvider {
|
|
16
|
+
return SEARCH_VERIFICATION_PROVIDERS.includes(provider as SearchVerificationProvider);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function verifySearchProvider(
|
|
20
|
+
provider: SearchVerificationProvider,
|
|
21
|
+
apiKey: string,
|
|
22
|
+
fetcher: typeof fetch = fetch,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
if (provider === 'serper' || provider === 'google') {
|
|
25
|
+
const res = await fetcher('https://google.serper.dev/search', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'X-API-KEY': apiKey,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ q: 'test', num: 1 }),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) throw new Error('Invalid Serper API Key');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (provider === 'tavily') {
|
|
38
|
+
const res = await fetcher('https://api.tavily.com/search', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({ api_key: apiKey, query: 'test', search_depth: 'basic' }),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) throw new Error('Invalid Tavily API Key');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (provider === 'brave') {
|
|
50
|
+
const res = await fetcher('https://api.search.brave.com/res/v1/web/search?q=test&count=1', {
|
|
51
|
+
headers: {
|
|
52
|
+
Accept: 'application/json',
|
|
53
|
+
'Accept-Encoding': 'gzip',
|
|
54
|
+
'X-Subscription-Token': apiKey,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) throw new Error('Invalid Brave API Key');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (provider === 'bing') {
|
|
62
|
+
const url = new URL('https://serpapi.com/search.json');
|
|
63
|
+
url.searchParams.set('engine', 'bing');
|
|
64
|
+
url.searchParams.set('q', 'test');
|
|
65
|
+
url.searchParams.set('api_key', apiKey);
|
|
66
|
+
|
|
67
|
+
const res = await fetcher(url);
|
|
68
|
+
if (!res.ok) throw new Error('Invalid SerpApi Key for Bing');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (provider === 'exa') {
|
|
73
|
+
const res = await fetcher('https://api.exa.ai/search', {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
accept: 'application/json',
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
'x-api-key': apiKey,
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({ query: 'test', numResults: 1 }),
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) throw new Error('Invalid Exa API Key');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const res = await fetcher('https://api.firecrawl.dev/v1/scrape', {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${apiKey}`,
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({ url: 'https://example.com', formats: ['markdown'] }),
|
|
93
|
+
});
|
|
94
|
+
if (res.status === 401) throw new Error('Invalid Firecrawl API Key');
|
|
95
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { SandboxManager } from '@larkup/sandbox';
|
|
3
|
+
import type { SandboxConfig } from '@larkup/sandbox/types';
|
|
4
|
+
export interface WorkspaceRuntimeHost {
|
|
5
|
+
readBinary?: (source: { id: string; url?: string }) => Promise<Uint8Array>;
|
|
6
|
+
apiUrl?: (path: string) => string;
|
|
7
|
+
fetch?: typeof globalThis.fetch;
|
|
8
|
+
sandbox: Pick<SandboxManager, 'execute' | 'healthCheck'>;
|
|
9
|
+
}
|
|
10
|
+
const runtime = new AsyncLocalStorage<WorkspaceRuntimeHost>();
|
|
11
|
+
export function runWithWorkspaceRuntime<T>(host: WorkspaceRuntimeHost, operation: () => T): T {
|
|
12
|
+
return runtime.run(host, operation);
|
|
13
|
+
}
|
|
14
|
+
export function workspaceSandbox(
|
|
15
|
+
config: SandboxConfig,
|
|
16
|
+
): Pick<SandboxManager, 'execute' | 'healthCheck'> {
|
|
17
|
+
return runtime.getStore()?.sandbox ?? new SandboxManager(config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function workspaceServerUrl(path: string): string {
|
|
21
|
+
return runtime.getStore()?.apiUrl?.(path) ?? path;
|
|
22
|
+
}
|
|
23
|
+
export const workspaceServerFetch: typeof globalThis.fetch = (input, init) =>
|
|
24
|
+
(runtime.getStore()?.fetch ?? globalThis.fetch)(input, init);
|
|
25
|
+
|
|
26
|
+
export function workspaceReadBinary(source: {
|
|
27
|
+
id: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
}): Promise<Uint8Array> | undefined {
|
|
30
|
+
return runtime.getStore()?.readBinary?.(source);
|
|
31
|
+
}
|