@agent-native/core 0.45.1 → 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.
Files changed (115) hide show
  1. package/README.md +1 -0
  2. package/dist/agent/production-agent.d.ts +28 -0
  3. package/dist/agent/production-agent.d.ts.map +1 -1
  4. package/dist/agent/production-agent.js +14 -7
  5. package/dist/agent/production-agent.js.map +1 -1
  6. package/dist/cli/skills.d.ts +2 -2
  7. package/dist/cli/skills.d.ts.map +1 -1
  8. package/dist/cli/skills.js +33 -0
  9. package/dist/cli/skills.js.map +1 -1
  10. package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
  11. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
  12. package/dist/client/components/LiveCursorOverlay.js +137 -0
  13. package/dist/client/components/LiveCursorOverlay.js.map +1 -0
  14. package/dist/client/components/PresenceBar.d.ts +11 -1
  15. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  16. package/dist/client/components/PresenceBar.js +39 -7
  17. package/dist/client/components/PresenceBar.js.map +1 -1
  18. package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
  19. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
  20. package/dist/client/components/RemoteSelectionRings.js +116 -0
  21. package/dist/client/components/RemoteSelectionRings.js.map +1 -0
  22. package/dist/client/index.d.ts +4 -0
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +5 -0
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/coding-tools/run-code.d.ts +40 -0
  27. package/dist/coding-tools/run-code.d.ts.map +1 -0
  28. package/dist/coding-tools/run-code.js +511 -0
  29. package/dist/coding-tools/run-code.js.map +1 -0
  30. package/dist/collab/awareness.d.ts +25 -0
  31. package/dist/collab/awareness.d.ts.map +1 -1
  32. package/dist/collab/awareness.js +42 -5
  33. package/dist/collab/awareness.js.map +1 -1
  34. package/dist/collab/client.d.ts +19 -1
  35. package/dist/collab/client.d.ts.map +1 -1
  36. package/dist/collab/client.js +362 -57
  37. package/dist/collab/client.js.map +1 -1
  38. package/dist/collab/follow-mode.d.ts +56 -0
  39. package/dist/collab/follow-mode.d.ts.map +1 -0
  40. package/dist/collab/follow-mode.js +54 -0
  41. package/dist/collab/follow-mode.js.map +1 -0
  42. package/dist/collab/index.d.ts +3 -1
  43. package/dist/collab/index.d.ts.map +1 -1
  44. package/dist/collab/index.js +5 -1
  45. package/dist/collab/index.js.map +1 -1
  46. package/dist/collab/presence.d.ts +56 -0
  47. package/dist/collab/presence.d.ts.map +1 -0
  48. package/dist/collab/presence.js +98 -0
  49. package/dist/collab/presence.js.map +1 -0
  50. package/dist/collab/routes.d.ts.map +1 -1
  51. package/dist/collab/routes.js +33 -6
  52. package/dist/collab/routes.js.map +1 -1
  53. package/dist/collab/struct-routes.d.ts.map +1 -1
  54. package/dist/collab/struct-routes.js +24 -4
  55. package/dist/collab/struct-routes.js.map +1 -1
  56. package/dist/collab/ydoc-manager.d.ts +13 -0
  57. package/dist/collab/ydoc-manager.d.ts.map +1 -1
  58. package/dist/collab/ydoc-manager.js +51 -15
  59. package/dist/collab/ydoc-manager.js.map +1 -1
  60. package/dist/extensions/fetch-tool.d.ts.map +1 -1
  61. package/dist/extensions/fetch-tool.js +62 -7
  62. package/dist/extensions/fetch-tool.js.map +1 -1
  63. package/dist/extensions/web-search-tool.d.ts +41 -0
  64. package/dist/extensions/web-search-tool.d.ts.map +1 -0
  65. package/dist/extensions/web-search-tool.js +200 -0
  66. package/dist/extensions/web-search-tool.js.map +1 -0
  67. package/dist/provider-api/custom-registry.d.ts +92 -0
  68. package/dist/provider-api/custom-registry.d.ts.map +1 -0
  69. package/dist/provider-api/custom-registry.js +289 -0
  70. package/dist/provider-api/custom-registry.js.map +1 -0
  71. package/dist/provider-api/index.d.ts +80 -44
  72. package/dist/provider-api/index.d.ts.map +1 -1
  73. package/dist/provider-api/index.js +569 -18
  74. package/dist/provider-api/index.js.map +1 -1
  75. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  76. package/dist/secrets/register-framework-secrets.js +36 -3
  77. package/dist/secrets/register-framework-secrets.js.map +1 -1
  78. package/dist/server/agent-chat-plugin.d.ts +36 -0
  79. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  80. package/dist/server/agent-chat-plugin.js +119 -0
  81. package/dist/server/agent-chat-plugin.js.map +1 -1
  82. package/dist/server/collab-plugin.d.ts +6 -0
  83. package/dist/server/collab-plugin.d.ts.map +1 -1
  84. package/dist/server/collab-plugin.js +105 -5
  85. package/dist/server/collab-plugin.js.map +1 -1
  86. package/dist/server/poll-events.d.ts +5 -0
  87. package/dist/server/poll-events.d.ts.map +1 -1
  88. package/dist/server/poll-events.js +27 -4
  89. package/dist/server/poll-events.js.map +1 -1
  90. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  91. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  92. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  93. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
  94. package/dist/workspace-files/index.d.ts +4 -0
  95. package/dist/workspace-files/index.d.ts.map +1 -0
  96. package/dist/workspace-files/index.js +4 -0
  97. package/dist/workspace-files/index.js.map +1 -0
  98. package/dist/workspace-files/schema.d.ts +195 -0
  99. package/dist/workspace-files/schema.d.ts.map +1 -0
  100. package/dist/workspace-files/schema.js +48 -0
  101. package/dist/workspace-files/schema.js.map +1 -0
  102. package/dist/workspace-files/store.d.ts +89 -0
  103. package/dist/workspace-files/store.d.ts.map +1 -0
  104. package/dist/workspace-files/store.js +298 -0
  105. package/dist/workspace-files/store.js.map +1 -0
  106. package/dist/workspace-files/tool.d.ts +15 -0
  107. package/dist/workspace-files/tool.d.ts.map +1 -0
  108. package/dist/workspace-files/tool.js +226 -0
  109. package/dist/workspace-files/tool.js.map +1 -0
  110. package/docs/content/real-time-collaboration.md +481 -97
  111. package/package.json +2 -1
  112. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  113. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  114. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  115. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
@@ -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) => listProviderApiCatalog(provider, { providerIds }),
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
- assertProviderAllowed(options.provider, runtime.providerIds);
726
- const config = getProviderApiConfig(options.provider);
727
- const catalog = listProviderApiCatalog(options.provider)[0];
728
- if (!options.url)
729
- return { provider: config.id, catalog };
730
- const url = new URL(options.url);
731
- const allowed = [
732
- ...config.docsUrls,
733
- ...(config.specUrls ?? []),
734
- config.defaultBaseUrl,
735
- ].some((allowedUrl) => sameOriginOrChild(url, new URL(allowedUrl)));
736
- if (!allowed) {
737
- throw new Error(`Docs URL must be one of the registered ${config.label} docs/spec origins.`);
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: config.id,
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
- assertProviderAllowed(args.provider, runtime.providerIds);
755
- const config = getProviderApiConfig(args.provider);
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: clampMaxBytes(args.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
  }