@agent-native/core 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/production-agent.d.ts +28 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/skills.d.ts +2 -2
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +33 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts +40 -0
- package/dist/coding-tools/run-code.d.ts.map +1 -0
- package/dist/coding-tools/run-code.js +511 -0
- package/dist/coding-tools/run-code.js.map +1 -0
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +62 -7
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/extensions/web-search-tool.d.ts +41 -0
- package/dist/extensions/web-search-tool.d.ts.map +1 -0
- package/dist/extensions/web-search-tool.js +200 -0
- package/dist/extensions/web-search-tool.js.map +1 -0
- package/dist/provider-api/custom-registry.d.ts +92 -0
- package/dist/provider-api/custom-registry.d.ts.map +1 -0
- package/dist/provider-api/custom-registry.js +289 -0
- package/dist/provider-api/custom-registry.js.map +1 -0
- package/dist/provider-api/index.d.ts +80 -44
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +569 -18
- package/dist/provider-api/index.js.map +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +36 -3
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +36 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +119 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/workspace-files/index.d.ts +4 -0
- package/dist/workspace-files/index.d.ts.map +1 -0
- package/dist/workspace-files/index.js +4 -0
- package/dist/workspace-files/index.js.map +1 -0
- package/dist/workspace-files/schema.d.ts +195 -0
- package/dist/workspace-files/schema.d.ts.map +1 -0
- package/dist/workspace-files/schema.js +48 -0
- package/dist/workspace-files/schema.js.map +1 -0
- package/dist/workspace-files/store.d.ts +89 -0
- package/dist/workspace-files/store.d.ts.map +1 -0
- package/dist/workspace-files/store.js +298 -0
- package/dist/workspace-files/store.js.map +1 -0
- package/dist/workspace-files/tool.d.ts +15 -0
- package/dist/workspace-files/tool.d.ts.map +1 -0
- package/dist/workspace-files/tool.js +226 -0
- package/dist/workspace-files/tool.js.map +1 -0
- package/package.json +2 -1
|
@@ -4,6 +4,7 @@ import { createSsrfSafeDispatcher, isBlockedExtensionUrlWithDns, } from "../exte
|
|
|
4
4
|
import { listOAuthAccountsByOwner, saveOAuthTokens, } from "../oauth-tokens/index.js";
|
|
5
5
|
import { getCredentialContext } from "../server/request-context.js";
|
|
6
6
|
import { resolveWorkspaceConnectionCredentialForApp } from "../workspace-connections/credentials.js";
|
|
7
|
+
export { upsertCustomProvider, deleteCustomProvider, listCustomProviders, getCustomProvider, validateCustomBaseUrl, } from "./custom-registry.js";
|
|
7
8
|
export const PROVIDER_API_IDS = [
|
|
8
9
|
"amplitude",
|
|
9
10
|
"apollo",
|
|
@@ -35,6 +36,10 @@ const DEFAULT_TIMEOUT_MS = 30_000;
|
|
|
35
36
|
const MAX_TIMEOUT_MS = 120_000;
|
|
36
37
|
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
37
38
|
const MAX_MAX_BYTES = 4 * 1024 * 1024;
|
|
39
|
+
/** When saveToFile is used, allow a much larger per-page response since the
|
|
40
|
+
* content won't enter the model's context window. */
|
|
41
|
+
const SAVE_TO_FILE_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
|
|
42
|
+
const FETCH_ALL_PAGES_MAX = 50;
|
|
38
43
|
const HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
39
44
|
const BLOCKED_OUTBOUND_HEADERS = new Set([
|
|
40
45
|
"connection",
|
|
@@ -716,25 +721,45 @@ export function createProviderApiRuntime(options) {
|
|
|
716
721
|
};
|
|
717
722
|
return {
|
|
718
723
|
providerIds,
|
|
719
|
-
listCatalog: (provider) =>
|
|
724
|
+
listCatalog: (provider) => listProviderApiCatalogWithCustom(provider, { providerIds }, runtimeOptions),
|
|
720
725
|
fetchDocs: (docsOptions) => fetchProviderApiDocs(docsOptions, runtimeOptions),
|
|
721
726
|
executeRequest: (args) => executeProviderApiRequest(args, runtimeOptions),
|
|
722
727
|
};
|
|
723
728
|
}
|
|
724
729
|
export async function fetchProviderApiDocs(options, runtime = { appId: "app" }) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
730
|
+
await assertProviderAllowedAsync(options.provider, runtime);
|
|
731
|
+
// Resolve config — may be a built-in or a custom provider.
|
|
732
|
+
const builtIn = isProviderApiId(options.provider)
|
|
733
|
+
? getProviderApiConfig(options.provider)
|
|
734
|
+
: null;
|
|
735
|
+
const customConfig = builtIn
|
|
736
|
+
? null
|
|
737
|
+
: await resolveCustomProvider(options.provider, runtime);
|
|
738
|
+
if (!builtIn && !customConfig) {
|
|
739
|
+
const known = await listAllProviderIds(runtime);
|
|
740
|
+
throw new Error(`Unknown provider "${options.provider}". Known providers: ${known.join(", ")}`);
|
|
741
|
+
}
|
|
742
|
+
const catalog = builtIn
|
|
743
|
+
? listProviderApiCatalog(options.provider)[0]
|
|
744
|
+
: customProviderToCatalogEntry(customConfig);
|
|
745
|
+
if (!options.url) {
|
|
746
|
+
return {
|
|
747
|
+
provider: options.provider,
|
|
748
|
+
catalog,
|
|
749
|
+
guidance: "provider-api-docs can fetch ANY public http(s) URL — pass url to retrieve API documentation, OpenAPI specs, changelogs, or any public web page. Registered docsUrls above are curated starting points.",
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
// Open docs fetching: allow ANY public https/http URL.
|
|
753
|
+
// The SSRF guard still applies — private/internal addresses are blocked.
|
|
754
|
+
let url;
|
|
755
|
+
try {
|
|
756
|
+
url = new URL(options.url);
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
throw new Error(`Invalid docs URL: ${options.url}`);
|
|
760
|
+
}
|
|
761
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
762
|
+
throw new Error(`Docs URL must use https: or http: (got ${url.protocol})`);
|
|
738
763
|
}
|
|
739
764
|
if (await isBlockedExtensionUrlWithDns(url.href)) {
|
|
740
765
|
throw new Error(`Blocked private/internal docs URL: ${url.href}`);
|
|
@@ -744,15 +769,30 @@ export async function fetchProviderApiDocs(options, runtime = { appId: "app" })
|
|
|
744
769
|
maxBytes: clampMaxBytes(options.maxBytes),
|
|
745
770
|
});
|
|
746
771
|
return {
|
|
747
|
-
provider:
|
|
772
|
+
provider: options.provider,
|
|
748
773
|
catalog,
|
|
749
774
|
request: { url: url.href },
|
|
750
775
|
response,
|
|
751
776
|
};
|
|
752
777
|
}
|
|
753
778
|
export async function executeProviderApiRequest(args, runtime) {
|
|
754
|
-
|
|
755
|
-
|
|
779
|
+
await assertProviderAllowedAsync(args.provider, runtime);
|
|
780
|
+
// Check whether this is a built-in or custom provider.
|
|
781
|
+
const builtIn = isProviderApiId(args.provider)
|
|
782
|
+
? getProviderApiConfig(args.provider)
|
|
783
|
+
: null;
|
|
784
|
+
const customConfig = builtIn
|
|
785
|
+
? null
|
|
786
|
+
: await resolveCustomProvider(args.provider, runtime);
|
|
787
|
+
if (!builtIn && !customConfig) {
|
|
788
|
+
const known = await listAllProviderIds(runtime);
|
|
789
|
+
throw new Error(`Unknown provider "${args.provider}". Known providers: ${known.join(", ")}`);
|
|
790
|
+
}
|
|
791
|
+
if (customConfig) {
|
|
792
|
+
return executeCustomProviderApiRequest(args, customConfig, runtime);
|
|
793
|
+
}
|
|
794
|
+
// --- built-in provider path (original code) ---
|
|
795
|
+
const config = builtIn;
|
|
756
796
|
const ctx = requireRuntimeCredentialContext(runtime, config.credentialKeys[0] ?? config.id);
|
|
757
797
|
const baseUrl = await resolveBaseUrl(config, runtime, ctx, args);
|
|
758
798
|
const placeholders = await resolvePlaceholders(config, runtime, ctx, args);
|
|
@@ -775,15 +815,78 @@ export async function executeProviderApiRequest(args, runtime) {
|
|
|
775
815
|
...(isPlainRecord(extraHeaders) ? extraHeaders : {}),
|
|
776
816
|
...auth.headers,
|
|
777
817
|
});
|
|
818
|
+
// Allow a much larger maxBytes ceiling when writing to a workspace file.
|
|
819
|
+
const effectiveMaxBytes = args.saveToFile
|
|
820
|
+
? SAVE_TO_FILE_MAX_BYTES
|
|
821
|
+
: clampMaxBytes(args.maxBytes);
|
|
822
|
+
// --- fetchAllPages mode ---
|
|
823
|
+
if (args.fetchAllPages) {
|
|
824
|
+
const pageCfg = args.fetchAllPages;
|
|
825
|
+
const { items, pageCount, lastStatus, lastContentType } = await fetchAllPages(pageCfg, async (extraQuery) => {
|
|
826
|
+
const queryWithCursor = extraQuery
|
|
827
|
+
? mergeQueryObjects(substituteUnknown(args.query, placeholders), extraQuery)
|
|
828
|
+
: substituteUnknown(args.query, placeholders);
|
|
829
|
+
const pageUrl = buildProviderUrl({
|
|
830
|
+
config,
|
|
831
|
+
baseUrl,
|
|
832
|
+
rawPath: substituteString(args.path, placeholders),
|
|
833
|
+
query: queryWithCursor,
|
|
834
|
+
});
|
|
835
|
+
const pageBody = prepareBody(substituteUnknown(args.body, placeholders), { ...headers });
|
|
836
|
+
const resp = await fetchWithTimeout(pageUrl.href, {
|
|
837
|
+
method,
|
|
838
|
+
headers,
|
|
839
|
+
body: pageBody,
|
|
840
|
+
maxBytes: effectiveMaxBytes,
|
|
841
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
842
|
+
secretValues: auth.secretValues,
|
|
843
|
+
});
|
|
844
|
+
return {
|
|
845
|
+
text: resp.text ??
|
|
846
|
+
(resp.json !== undefined ? JSON.stringify(resp.json) : ""),
|
|
847
|
+
contentType: resp.contentType,
|
|
848
|
+
status: resp.status,
|
|
849
|
+
ok: resp.ok,
|
|
850
|
+
};
|
|
851
|
+
});
|
|
852
|
+
const allItemsJson = JSON.stringify(items, null, 2);
|
|
853
|
+
const metadata = {
|
|
854
|
+
provider: { id: config.id, label: config.label },
|
|
855
|
+
pagesRead: pageCount,
|
|
856
|
+
totalItems: Array.isArray(items) ? items.length : 0,
|
|
857
|
+
lastStatus,
|
|
858
|
+
};
|
|
859
|
+
if (args.saveToFile) {
|
|
860
|
+
const saved = (await handleSaveToFile(args.saveToFile, allItemsJson, lastContentType ?? "application/json", lastStatus));
|
|
861
|
+
return { ...metadata, ...saved };
|
|
862
|
+
}
|
|
863
|
+
return { ...metadata, items };
|
|
864
|
+
}
|
|
865
|
+
// --- Single request ---
|
|
778
866
|
const body = prepareBody(substituteUnknown(args.body, placeholders), headers);
|
|
779
867
|
const response = await fetchWithTimeout(url.href, {
|
|
780
868
|
method,
|
|
781
869
|
headers,
|
|
782
870
|
body,
|
|
783
|
-
maxBytes:
|
|
871
|
+
maxBytes: effectiveMaxBytes,
|
|
784
872
|
timeoutMs: clampTimeout(args.timeoutMs),
|
|
785
873
|
secretValues: auth.secretValues,
|
|
786
874
|
});
|
|
875
|
+
// saveToFile: write full body to workspace file and return compact summary.
|
|
876
|
+
if (args.saveToFile) {
|
|
877
|
+
const rawText = response.text ??
|
|
878
|
+
(response.json !== undefined ? JSON.stringify(response.json) : "");
|
|
879
|
+
const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status));
|
|
880
|
+
return {
|
|
881
|
+
provider: { id: config.id, label: config.label },
|
|
882
|
+
request: {
|
|
883
|
+
method,
|
|
884
|
+
url: redactString(url.href, auth.secretValues),
|
|
885
|
+
path: redactString(`${url.pathname}${url.search}`, auth.secretValues),
|
|
886
|
+
},
|
|
887
|
+
...saved,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
787
890
|
return {
|
|
788
891
|
provider: {
|
|
789
892
|
id: config.id,
|
|
@@ -808,6 +911,342 @@ export async function executeProviderApiRequest(args, runtime) {
|
|
|
808
911
|
guidance: "This was a raw provider API request. Use provider docs/spec URLs to choose endpoints and include method/path/status plus relevant filters in the methodology. Prefer this escape hatch whenever canned actions are too narrow.",
|
|
809
912
|
};
|
|
810
913
|
}
|
|
914
|
+
// ---------------------------------------------------------------------------
|
|
915
|
+
// Custom provider execution
|
|
916
|
+
// ---------------------------------------------------------------------------
|
|
917
|
+
async function executeCustomProviderApiRequest(args, customConfig, runtime) {
|
|
918
|
+
const ctx = requireRuntimeCredentialContext(runtime, customConfig.id);
|
|
919
|
+
const method = normalizeMethod(args.method);
|
|
920
|
+
const baseUrl = customConfig.baseUrl;
|
|
921
|
+
// Build a lightweight ProviderApiConfig-like object so we can reuse
|
|
922
|
+
// buildProviderUrl (which validates allowed hosts).
|
|
923
|
+
const syntheticConfig = {
|
|
924
|
+
id: customConfig.id,
|
|
925
|
+
label: customConfig.label,
|
|
926
|
+
defaultBaseUrl: baseUrl,
|
|
927
|
+
auth: { type: "none" },
|
|
928
|
+
credentialKeys: [],
|
|
929
|
+
docsUrls: customConfig.docsUrls,
|
|
930
|
+
allowedHostSuffixes: customConfig.allowedHostSuffixes,
|
|
931
|
+
defaultHeaders: customConfig.defaultHeaders,
|
|
932
|
+
};
|
|
933
|
+
const url = buildProviderUrl({
|
|
934
|
+
config: syntheticConfig,
|
|
935
|
+
baseUrl,
|
|
936
|
+
rawPath: args.path,
|
|
937
|
+
query: args.query,
|
|
938
|
+
});
|
|
939
|
+
if (await isBlockedExtensionUrlWithDns(url.href)) {
|
|
940
|
+
throw new Error(`Blocked private/internal provider URL: ${url.href}`);
|
|
941
|
+
}
|
|
942
|
+
const auth = args.auth === "none"
|
|
943
|
+
? emptyAuth()
|
|
944
|
+
: await resolveCustomAuth(customConfig, runtime, ctx, args);
|
|
945
|
+
const extraHeaders = args.headers ?? {};
|
|
946
|
+
const headers = sanitizeOutboundHeaders({
|
|
947
|
+
...(customConfig.defaultHeaders ?? {}),
|
|
948
|
+
...(isPlainRecord(extraHeaders) ? extraHeaders : {}),
|
|
949
|
+
...auth.headers,
|
|
950
|
+
});
|
|
951
|
+
const body = prepareBody(args.body, headers);
|
|
952
|
+
const effectiveMaxBytes = args.saveToFile
|
|
953
|
+
? SAVE_TO_FILE_MAX_BYTES
|
|
954
|
+
: clampMaxBytes(args.maxBytes);
|
|
955
|
+
// --- fetchAllPages mode (same cursor pagination as built-in providers) ---
|
|
956
|
+
if (args.fetchAllPages) {
|
|
957
|
+
const pageCfg = args.fetchAllPages;
|
|
958
|
+
const { items, pageCount, lastStatus, lastContentType } = await fetchAllPages(pageCfg, async (extraQuery) => {
|
|
959
|
+
const queryWithCursor = extraQuery
|
|
960
|
+
? mergeQueryObjects(args.query, extraQuery)
|
|
961
|
+
: args.query;
|
|
962
|
+
const pageUrl = buildProviderUrl({
|
|
963
|
+
config: syntheticConfig,
|
|
964
|
+
baseUrl,
|
|
965
|
+
rawPath: args.path,
|
|
966
|
+
query: queryWithCursor,
|
|
967
|
+
});
|
|
968
|
+
const pageBody = prepareBody(args.body, { ...headers });
|
|
969
|
+
const resp = await fetchWithTimeout(pageUrl.href, {
|
|
970
|
+
method,
|
|
971
|
+
headers,
|
|
972
|
+
body: pageBody,
|
|
973
|
+
maxBytes: effectiveMaxBytes,
|
|
974
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
975
|
+
secretValues: auth.secretValues,
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
text: resp.text ??
|
|
979
|
+
(resp.json !== undefined ? JSON.stringify(resp.json) : ""),
|
|
980
|
+
contentType: resp.contentType,
|
|
981
|
+
status: resp.status,
|
|
982
|
+
ok: resp.ok,
|
|
983
|
+
};
|
|
984
|
+
});
|
|
985
|
+
const allItemsJson = JSON.stringify(items, null, 2);
|
|
986
|
+
const metadata = {
|
|
987
|
+
provider: {
|
|
988
|
+
id: customConfig.id,
|
|
989
|
+
label: customConfig.label,
|
|
990
|
+
custom: true,
|
|
991
|
+
},
|
|
992
|
+
pagesRead: pageCount,
|
|
993
|
+
totalItems: Array.isArray(items) ? items.length : 0,
|
|
994
|
+
lastStatus,
|
|
995
|
+
};
|
|
996
|
+
if (args.saveToFile) {
|
|
997
|
+
const saved = (await handleSaveToFile(args.saveToFile, allItemsJson, lastContentType ?? "application/json", lastStatus));
|
|
998
|
+
return { ...metadata, ...saved };
|
|
999
|
+
}
|
|
1000
|
+
return { ...metadata, items };
|
|
1001
|
+
}
|
|
1002
|
+
const response = await fetchWithTimeout(url.href, {
|
|
1003
|
+
method,
|
|
1004
|
+
headers,
|
|
1005
|
+
body,
|
|
1006
|
+
maxBytes: effectiveMaxBytes,
|
|
1007
|
+
timeoutMs: clampTimeout(args.timeoutMs),
|
|
1008
|
+
secretValues: auth.secretValues,
|
|
1009
|
+
});
|
|
1010
|
+
if (args.saveToFile) {
|
|
1011
|
+
const rawText = response.text ??
|
|
1012
|
+
(response.json !== undefined ? JSON.stringify(response.json) : "");
|
|
1013
|
+
const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status));
|
|
1014
|
+
return {
|
|
1015
|
+
provider: {
|
|
1016
|
+
id: customConfig.id,
|
|
1017
|
+
label: customConfig.label,
|
|
1018
|
+
custom: true,
|
|
1019
|
+
},
|
|
1020
|
+
request: {
|
|
1021
|
+
method,
|
|
1022
|
+
url: redactString(url.href, auth.secretValues),
|
|
1023
|
+
path: redactString(`${url.pathname}${url.search}`, auth.secretValues),
|
|
1024
|
+
},
|
|
1025
|
+
...saved,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
provider: {
|
|
1030
|
+
id: customConfig.id,
|
|
1031
|
+
label: customConfig.label,
|
|
1032
|
+
docsUrls: customConfig.docsUrls,
|
|
1033
|
+
specUrls: [],
|
|
1034
|
+
custom: true,
|
|
1035
|
+
},
|
|
1036
|
+
request: {
|
|
1037
|
+
method,
|
|
1038
|
+
url: redactString(url.href, auth.secretValues),
|
|
1039
|
+
path: redactString(`${url.pathname}${url.search}`, auth.secretValues),
|
|
1040
|
+
auth: args.auth === "none" ? "none" : describeCustomAuth(customConfig.auth),
|
|
1041
|
+
credentialSources: auth.credentialSources.map((source) => ({
|
|
1042
|
+
...source,
|
|
1043
|
+
fingerprint: fingerprint(source.key),
|
|
1044
|
+
})),
|
|
1045
|
+
headerNames: Object.keys(headers).filter((name) => name.toLowerCase() !== "authorization"),
|
|
1046
|
+
},
|
|
1047
|
+
response,
|
|
1048
|
+
guidance: "This was a raw provider API request to a custom provider. Use provider docs URLs to choose endpoints.",
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
async function resolveCustomAuth(customConfig, runtime, ctx, args) {
|
|
1052
|
+
const auth = customConfig.auth;
|
|
1053
|
+
if (auth.type === "none")
|
|
1054
|
+
return emptyAuth();
|
|
1055
|
+
if (auth.type === "bearer") {
|
|
1056
|
+
const credential = await resolveRequiredCredentialByKey({
|
|
1057
|
+
provider: customConfig.id,
|
|
1058
|
+
key: auth.credentialKey,
|
|
1059
|
+
ctx,
|
|
1060
|
+
runtime,
|
|
1061
|
+
connectionId: args.connectionId,
|
|
1062
|
+
});
|
|
1063
|
+
return {
|
|
1064
|
+
headers: { Authorization: `Bearer ${credential.value}` },
|
|
1065
|
+
credentialSources: [omitCredentialValue(credential)],
|
|
1066
|
+
secretValues: [credential.value],
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
if (auth.type === "basic") {
|
|
1070
|
+
const username = await resolveRequiredCredentialByKey({
|
|
1071
|
+
provider: customConfig.id,
|
|
1072
|
+
key: auth.usernameKey,
|
|
1073
|
+
ctx,
|
|
1074
|
+
runtime,
|
|
1075
|
+
connectionId: args.connectionId,
|
|
1076
|
+
});
|
|
1077
|
+
const password = auth.passwordKey === auth.usernameKey
|
|
1078
|
+
? username
|
|
1079
|
+
: await resolveRequiredCredentialByKey({
|
|
1080
|
+
provider: customConfig.id,
|
|
1081
|
+
key: auth.passwordKey,
|
|
1082
|
+
ctx,
|
|
1083
|
+
runtime,
|
|
1084
|
+
connectionId: args.connectionId,
|
|
1085
|
+
});
|
|
1086
|
+
const encoded = Buffer.from(`${username.value}:${password.value}`).toString("base64");
|
|
1087
|
+
return {
|
|
1088
|
+
headers: { Authorization: `Basic ${encoded}` },
|
|
1089
|
+
credentialSources: [
|
|
1090
|
+
omitCredentialValue(username),
|
|
1091
|
+
...(password.key === username.key
|
|
1092
|
+
? []
|
|
1093
|
+
: [omitCredentialValue(password)]),
|
|
1094
|
+
],
|
|
1095
|
+
secretValues: [username.value, password.value, encoded],
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
if (auth.type === "api-key-header") {
|
|
1099
|
+
const credential = await resolveRequiredCredentialByKey({
|
|
1100
|
+
provider: customConfig.id,
|
|
1101
|
+
key: auth.credentialKey,
|
|
1102
|
+
ctx,
|
|
1103
|
+
runtime,
|
|
1104
|
+
connectionId: args.connectionId,
|
|
1105
|
+
});
|
|
1106
|
+
return {
|
|
1107
|
+
headers: { [auth.headerName]: credential.value },
|
|
1108
|
+
credentialSources: [omitCredentialValue(credential)],
|
|
1109
|
+
secretValues: [credential.value],
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
return emptyAuth();
|
|
1113
|
+
}
|
|
1114
|
+
/** Resolve a credential by key name (no workspace-provider lookup for custom). */
|
|
1115
|
+
async function resolveRequiredCredentialByKey(options) {
|
|
1116
|
+
const localCredentialSource = options.runtime.localCredentialSource ?? "app_local";
|
|
1117
|
+
const lookup = {
|
|
1118
|
+
appId: options.runtime.appId,
|
|
1119
|
+
provider: options.provider,
|
|
1120
|
+
key: options.key,
|
|
1121
|
+
ctx: options.ctx,
|
|
1122
|
+
workspaceProvider: undefined,
|
|
1123
|
+
connectionId: options.connectionId,
|
|
1124
|
+
localCredentialSource,
|
|
1125
|
+
};
|
|
1126
|
+
const resolver = options.runtime.resolveCredential ?? defaultProviderApiCredentialResolver;
|
|
1127
|
+
const credential = await resolver(lookup);
|
|
1128
|
+
if (!credential?.value) {
|
|
1129
|
+
throw new Error(`Credential "${options.key}" not configured for custom provider "${options.provider}".`);
|
|
1130
|
+
}
|
|
1131
|
+
return credential;
|
|
1132
|
+
}
|
|
1133
|
+
function describeCustomAuth(auth) {
|
|
1134
|
+
if (auth.type === "none")
|
|
1135
|
+
return "none";
|
|
1136
|
+
if (auth.type === "bearer")
|
|
1137
|
+
return "bearer";
|
|
1138
|
+
if (auth.type === "basic")
|
|
1139
|
+
return "basic";
|
|
1140
|
+
if (auth.type === "api-key-header")
|
|
1141
|
+
return `api-key-header:${auth.headerName}`;
|
|
1142
|
+
return "unknown";
|
|
1143
|
+
}
|
|
1144
|
+
// ---------------------------------------------------------------------------
|
|
1145
|
+
// Catalog helpers with custom provider support
|
|
1146
|
+
// ---------------------------------------------------------------------------
|
|
1147
|
+
/**
|
|
1148
|
+
* Convert a custom provider to the same catalog shape as built-in providers.
|
|
1149
|
+
*/
|
|
1150
|
+
function customProviderToCatalogEntry(config) {
|
|
1151
|
+
return {
|
|
1152
|
+
id: config.id,
|
|
1153
|
+
label: config.label,
|
|
1154
|
+
defaultBaseUrl: config.baseUrl,
|
|
1155
|
+
baseUrlCredentialKey: null,
|
|
1156
|
+
auth: describeCustomAuth(config.auth),
|
|
1157
|
+
credentialKeys: extractCredentialKeysFromCustomAuth(config.auth),
|
|
1158
|
+
docsUrls: config.docsUrls,
|
|
1159
|
+
specUrls: [],
|
|
1160
|
+
allowedHostSuffixes: config.allowedHostSuffixes,
|
|
1161
|
+
placeholders: [],
|
|
1162
|
+
defaultHeaders: config.defaultHeaders,
|
|
1163
|
+
examples: [],
|
|
1164
|
+
notes: config.notes ? [config.notes] : [],
|
|
1165
|
+
templateUses: [],
|
|
1166
|
+
custom: true,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function extractCredentialKeysFromCustomAuth(auth) {
|
|
1170
|
+
if (auth.type === "bearer")
|
|
1171
|
+
return [auth.credentialKey];
|
|
1172
|
+
if (auth.type === "basic") {
|
|
1173
|
+
return auth.usernameKey === auth.passwordKey
|
|
1174
|
+
? [auth.usernameKey]
|
|
1175
|
+
: [auth.usernameKey, auth.passwordKey];
|
|
1176
|
+
}
|
|
1177
|
+
if (auth.type === "api-key-header")
|
|
1178
|
+
return [auth.credentialKey];
|
|
1179
|
+
return [];
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* List catalog entries including custom providers (merged after built-ins).
|
|
1183
|
+
*/
|
|
1184
|
+
async function listProviderApiCatalogWithCustom(provider, options, runtime) {
|
|
1185
|
+
const customConfigs = runtime.getCustomProviders
|
|
1186
|
+
? await runtime.getCustomProviders()
|
|
1187
|
+
: [];
|
|
1188
|
+
if (provider) {
|
|
1189
|
+
// Check built-ins first
|
|
1190
|
+
if (isProviderApiId(provider)) {
|
|
1191
|
+
return listProviderApiCatalog(provider, options);
|
|
1192
|
+
}
|
|
1193
|
+
// Check custom
|
|
1194
|
+
const custom = customConfigs.find((c) => c.id === provider);
|
|
1195
|
+
if (custom)
|
|
1196
|
+
return [customProviderToCatalogEntry(custom)];
|
|
1197
|
+
const known = [
|
|
1198
|
+
...normalizeProviderIds(options.providerIds),
|
|
1199
|
+
...customConfigs.map((c) => c.id),
|
|
1200
|
+
];
|
|
1201
|
+
throw new Error(`Unknown provider "${provider}". Known providers: ${known.join(", ")}`);
|
|
1202
|
+
}
|
|
1203
|
+
const builtInEntries = listProviderApiCatalog(undefined, options);
|
|
1204
|
+
const builtInIds = new Set((options.providerIds ?? PROVIDER_API_IDS).map(String));
|
|
1205
|
+
const customEntries = customConfigs
|
|
1206
|
+
.filter((c) => !builtInIds.has(c.id))
|
|
1207
|
+
.map(customProviderToCatalogEntry);
|
|
1208
|
+
return [...builtInEntries, ...customEntries];
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Look up a custom provider by id from the runtime loader.
|
|
1212
|
+
*/
|
|
1213
|
+
async function resolveCustomProvider(id, runtime) {
|
|
1214
|
+
if (!runtime.getCustomProviders)
|
|
1215
|
+
return null;
|
|
1216
|
+
const configs = await runtime.getCustomProviders();
|
|
1217
|
+
return configs.find((c) => c.id === id) ?? null;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* List all provider ids (built-in + custom) visible to this runtime.
|
|
1221
|
+
*/
|
|
1222
|
+
async function listAllProviderIds(runtime) {
|
|
1223
|
+
const builtIn = normalizeProviderIds(runtime.providerIds).map(String);
|
|
1224
|
+
if (!runtime.getCustomProviders)
|
|
1225
|
+
return builtIn;
|
|
1226
|
+
const custom = await runtime.getCustomProviders();
|
|
1227
|
+
return [...builtIn, ...custom.map((c) => c.id)];
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Assert that a provider is either a known built-in or a registered custom
|
|
1231
|
+
* provider. Throws with a descriptive message listing known providers.
|
|
1232
|
+
*/
|
|
1233
|
+
async function assertProviderAllowedAsync(provider, runtime) {
|
|
1234
|
+
// Built-in check (fast path)
|
|
1235
|
+
if (isProviderApiId(provider)) {
|
|
1236
|
+
// Still check the providerIds whitelist if set
|
|
1237
|
+
const allowed = normalizeProviderIds(runtime.providerIds);
|
|
1238
|
+
if (!allowed.includes(provider)) {
|
|
1239
|
+
throw new Error(`Provider API ${provider} is not enabled for this app.`);
|
|
1240
|
+
}
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
// Custom provider check
|
|
1244
|
+
const custom = await resolveCustomProvider(provider, runtime);
|
|
1245
|
+
if (custom)
|
|
1246
|
+
return;
|
|
1247
|
+
const known = await listAllProviderIds(runtime);
|
|
1248
|
+
throw new Error(`Unknown provider "${provider}". Known providers: ${known.join(", ")}`);
|
|
1249
|
+
}
|
|
811
1250
|
export async function defaultProviderApiCredentialResolver(options) {
|
|
812
1251
|
if (options.workspaceProvider) {
|
|
813
1252
|
const result = await resolveWorkspaceConnectionCredentialForApp({
|
|
@@ -1479,6 +1918,21 @@ function redactString(text, secretValues) {
|
|
|
1479
1918
|
}
|
|
1480
1919
|
return output;
|
|
1481
1920
|
}
|
|
1921
|
+
function mergeQueryObjects(base, extra) {
|
|
1922
|
+
if (!base)
|
|
1923
|
+
return extra;
|
|
1924
|
+
if (typeof base === "string") {
|
|
1925
|
+
const params = new URLSearchParams(base.replace(/^\?/, ""));
|
|
1926
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
1927
|
+
params.set(key, value);
|
|
1928
|
+
}
|
|
1929
|
+
return params.toString();
|
|
1930
|
+
}
|
|
1931
|
+
if (typeof base === "object" && !Array.isArray(base)) {
|
|
1932
|
+
return { ...base, ...extra };
|
|
1933
|
+
}
|
|
1934
|
+
return extra;
|
|
1935
|
+
}
|
|
1482
1936
|
function clampTimeout(timeoutMs) {
|
|
1483
1937
|
if (!Number.isFinite(timeoutMs))
|
|
1484
1938
|
return DEFAULT_TIMEOUT_MS;
|
|
@@ -1489,6 +1943,103 @@ function clampMaxBytes(maxBytes) {
|
|
|
1489
1943
|
return DEFAULT_MAX_BYTES;
|
|
1490
1944
|
return Math.max(1_000, Math.min(MAX_MAX_BYTES, Math.floor(maxBytes)));
|
|
1491
1945
|
}
|
|
1946
|
+
/** Resolve a dot-path from a parsed JSON object, e.g. "meta.next_cursor". */
|
|
1947
|
+
function dotGet(obj, path) {
|
|
1948
|
+
if (!path)
|
|
1949
|
+
return obj;
|
|
1950
|
+
let current = obj;
|
|
1951
|
+
for (const key of path.split(".")) {
|
|
1952
|
+
if (current === null || typeof current !== "object")
|
|
1953
|
+
return undefined;
|
|
1954
|
+
current = current[key];
|
|
1955
|
+
}
|
|
1956
|
+
return current;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Handle saveToFile: write the full provider-api response body to a workspace
|
|
1960
|
+
* file and return a compact summary.
|
|
1961
|
+
*/
|
|
1962
|
+
async function handleSaveToFile(filePath, responseText, contentType, status) {
|
|
1963
|
+
const { writeWorkspaceFile, SAVE_TO_FILE_MAX_BYTES: maxSaveBytes } = await import("../workspace-files/store.js");
|
|
1964
|
+
const { getRequestOrgId, getRequestUserEmail } = await import("../server/request-context.js");
|
|
1965
|
+
const orgId = getRequestOrgId();
|
|
1966
|
+
const email = getRequestUserEmail();
|
|
1967
|
+
const scope = orgId
|
|
1968
|
+
? { scope: "org", scopeId: orgId }
|
|
1969
|
+
: email
|
|
1970
|
+
? { scope: "user", scopeId: email }
|
|
1971
|
+
: null;
|
|
1972
|
+
if (!scope) {
|
|
1973
|
+
throw new Error("saveToFile requires an authenticated request context (no user email or orgId found).");
|
|
1974
|
+
}
|
|
1975
|
+
const mimeType = contentType?.split(";")[0].trim() ?? "text/plain";
|
|
1976
|
+
await writeWorkspaceFile(scope, filePath, responseText, mimeType, {
|
|
1977
|
+
maxFileBytes: maxSaveBytes,
|
|
1978
|
+
});
|
|
1979
|
+
const bytes = Buffer.byteLength(responseText, "utf8");
|
|
1980
|
+
const preview = responseText.slice(0, 2000);
|
|
1981
|
+
return {
|
|
1982
|
+
savedToFile: true,
|
|
1983
|
+
savedTo: filePath,
|
|
1984
|
+
status,
|
|
1985
|
+
bytes,
|
|
1986
|
+
contentType: mimeType,
|
|
1987
|
+
preview: preview.length < responseText.length ? `${preview}…` : preview,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Execute paginated requests, accumulating items across pages.
|
|
1992
|
+
* Returns the accumulated items array and the last response for metadata.
|
|
1993
|
+
*/
|
|
1994
|
+
async function fetchAllPages(config, executeOnePage) {
|
|
1995
|
+
const maxPages = Math.min(Number.isFinite(config.maxPages) && config.maxPages > 0
|
|
1996
|
+
? config.maxPages
|
|
1997
|
+
: 10, FETCH_ALL_PAGES_MAX);
|
|
1998
|
+
const items = [];
|
|
1999
|
+
let cursor;
|
|
2000
|
+
let pageCount = 0;
|
|
2001
|
+
let lastStatus = 0;
|
|
2002
|
+
let lastContentType = null;
|
|
2003
|
+
while (pageCount < maxPages) {
|
|
2004
|
+
const extraQuery = {};
|
|
2005
|
+
if (cursor)
|
|
2006
|
+
extraQuery[config.cursorParam] = cursor;
|
|
2007
|
+
const page = await executeOnePage(pageCount > 0 ? extraQuery : undefined);
|
|
2008
|
+
lastStatus = page.status;
|
|
2009
|
+
lastContentType = page.contentType;
|
|
2010
|
+
pageCount++;
|
|
2011
|
+
let body;
|
|
2012
|
+
try {
|
|
2013
|
+
body = JSON.parse(page.text);
|
|
2014
|
+
}
|
|
2015
|
+
catch {
|
|
2016
|
+
body = page.text;
|
|
2017
|
+
}
|
|
2018
|
+
// Extract items
|
|
2019
|
+
if (config.itemsPath) {
|
|
2020
|
+
const extracted = dotGet(body, config.itemsPath);
|
|
2021
|
+
if (Array.isArray(extracted)) {
|
|
2022
|
+
items.push(...extracted);
|
|
2023
|
+
}
|
|
2024
|
+
else if (extracted !== undefined) {
|
|
2025
|
+
items.push(extracted);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
else {
|
|
2029
|
+
items.push(body);
|
|
2030
|
+
}
|
|
2031
|
+
// Extract next cursor
|
|
2032
|
+
const nextCursor = dotGet(body, config.cursorPath);
|
|
2033
|
+
if (!nextCursor ||
|
|
2034
|
+
nextCursor === "" ||
|
|
2035
|
+
nextCursor === null ||
|
|
2036
|
+
nextCursor === cursor) {
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
cursor = String(nextCursor);
|
|
2040
|
+
}
|
|
2041
|
+
return { items, pageCount, lastStatus, lastContentType };
|
|
2042
|
+
}
|
|
1492
2043
|
function fingerprint(value) {
|
|
1493
2044
|
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
1494
2045
|
}
|