@aigne/afs-cli 1.11.0-beta.10 → 1.11.0-beta.12

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 (154) hide show
  1. package/dist/cli.cjs +3 -2
  2. package/dist/cli.mjs +3 -2
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/config/afs-loader.cjs +64 -315
  5. package/dist/config/afs-loader.d.cts.map +1 -1
  6. package/dist/config/afs-loader.d.mts +2 -1
  7. package/dist/config/afs-loader.d.mts.map +1 -1
  8. package/dist/config/afs-loader.mjs +59 -310
  9. package/dist/config/afs-loader.mjs.map +1 -1
  10. package/dist/config/credential-helpers.cjs +291 -0
  11. package/dist/config/credential-helpers.d.mts +2 -0
  12. package/dist/config/credential-helpers.mjs +288 -0
  13. package/dist/config/credential-helpers.mjs.map +1 -0
  14. package/dist/config/loader.cjs +3 -1
  15. package/dist/config/loader.mjs +3 -2
  16. package/dist/config/loader.mjs.map +1 -1
  17. package/dist/config/program-install.cjs +276 -0
  18. package/dist/config/program-install.d.mts +1 -0
  19. package/dist/config/program-install.mjs +273 -0
  20. package/dist/config/program-install.mjs.map +1 -0
  21. package/dist/core/commands/connect.cjs +53 -0
  22. package/dist/core/commands/connect.d.mts +2 -0
  23. package/dist/core/commands/connect.mjs +55 -0
  24. package/dist/core/commands/connect.mjs.map +1 -0
  25. package/dist/core/commands/daemon.cjs +207 -0
  26. package/dist/core/commands/daemon.d.mts +2 -0
  27. package/dist/core/commands/daemon.mjs +208 -0
  28. package/dist/core/commands/daemon.mjs.map +1 -0
  29. package/dist/core/commands/explain.cjs +3 -1
  30. package/dist/core/commands/explain.mjs +3 -1
  31. package/dist/core/commands/explain.mjs.map +1 -1
  32. package/dist/core/commands/explore.cjs +47 -12
  33. package/dist/core/commands/explore.mjs +47 -12
  34. package/dist/core/commands/explore.mjs.map +1 -1
  35. package/dist/core/commands/gen-agent-md.cjs +126 -0
  36. package/dist/core/commands/gen-agent-md.d.mts +2 -0
  37. package/dist/core/commands/gen-agent-md.mjs +125 -0
  38. package/dist/core/commands/gen-agent-md.mjs.map +1 -0
  39. package/dist/core/commands/index.cjs +13 -1
  40. package/dist/core/commands/index.d.cts.map +1 -1
  41. package/dist/core/commands/index.d.mts +6 -0
  42. package/dist/core/commands/index.d.mts.map +1 -1
  43. package/dist/core/commands/index.mjs +13 -1
  44. package/dist/core/commands/index.mjs.map +1 -1
  45. package/dist/core/commands/install.cjs +91 -0
  46. package/dist/core/commands/install.d.mts +2 -0
  47. package/dist/core/commands/install.mjs +92 -0
  48. package/dist/core/commands/install.mjs.map +1 -0
  49. package/dist/core/commands/ls.cjs +14 -2
  50. package/dist/core/commands/ls.d.cts +2 -0
  51. package/dist/core/commands/ls.d.cts.map +1 -1
  52. package/dist/core/commands/ls.d.mts +2 -0
  53. package/dist/core/commands/ls.d.mts.map +1 -1
  54. package/dist/core/commands/ls.mjs +14 -2
  55. package/dist/core/commands/ls.mjs.map +1 -1
  56. package/dist/core/commands/mcp-bridge.cjs +201 -0
  57. package/dist/core/commands/mcp-bridge.d.mts +2 -0
  58. package/dist/core/commands/mcp-bridge.mjs +201 -0
  59. package/dist/core/commands/mcp-bridge.mjs.map +1 -0
  60. package/dist/core/commands/read.cjs +20 -7
  61. package/dist/core/commands/read.d.cts +2 -0
  62. package/dist/core/commands/read.d.cts.map +1 -1
  63. package/dist/core/commands/read.d.mts +2 -0
  64. package/dist/core/commands/read.d.mts.map +1 -1
  65. package/dist/core/commands/read.mjs +20 -7
  66. package/dist/core/commands/read.mjs.map +1 -1
  67. package/dist/core/commands/search.cjs +5 -1
  68. package/dist/core/commands/search.mjs +5 -1
  69. package/dist/core/commands/search.mjs.map +1 -1
  70. package/dist/core/commands/stat.mjs.map +1 -1
  71. package/dist/core/commands/types.d.cts +2 -0
  72. package/dist/core/commands/types.d.cts.map +1 -1
  73. package/dist/core/commands/types.d.mts +2 -0
  74. package/dist/core/commands/types.d.mts.map +1 -1
  75. package/dist/core/commands/types.mjs.map +1 -1
  76. package/dist/core/commands/vault.cjs +289 -0
  77. package/dist/core/commands/vault.d.mts +2 -0
  78. package/dist/core/commands/vault.mjs +289 -0
  79. package/dist/core/commands/vault.mjs.map +1 -0
  80. package/dist/core/commands/write.cjs +19 -6
  81. package/dist/core/commands/write.d.cts +2 -1
  82. package/dist/core/commands/write.d.cts.map +1 -1
  83. package/dist/core/commands/write.d.mts +2 -1
  84. package/dist/core/commands/write.d.mts.map +1 -1
  85. package/dist/core/commands/write.mjs +19 -6
  86. package/dist/core/commands/write.mjs.map +1 -1
  87. package/dist/core/executor/index.cjs +95 -19
  88. package/dist/core/executor/index.d.cts +4 -0
  89. package/dist/core/executor/index.d.cts.map +1 -1
  90. package/dist/core/executor/index.d.mts +4 -0
  91. package/dist/core/executor/index.d.mts.map +1 -1
  92. package/dist/core/executor/index.mjs +95 -19
  93. package/dist/core/executor/index.mjs.map +1 -1
  94. package/dist/core/formatters/index.d.mts +1 -0
  95. package/dist/core/formatters/install.cjs +21 -0
  96. package/dist/core/formatters/install.d.mts +1 -0
  97. package/dist/core/formatters/install.mjs +19 -0
  98. package/dist/core/formatters/install.mjs.map +1 -0
  99. package/dist/core/formatters/vault.cjs +36 -0
  100. package/dist/core/formatters/vault.mjs +32 -0
  101. package/dist/core/formatters/vault.mjs.map +1 -0
  102. package/dist/credential/index.d.mts +2 -1
  103. package/dist/credential/mcp-auth-context.cjs +27 -5
  104. package/dist/credential/mcp-auth-context.mjs +27 -5
  105. package/dist/credential/mcp-auth-context.mjs.map +1 -1
  106. package/dist/credential/resolver.cjs +7 -2
  107. package/dist/credential/resolver.mjs +7 -2
  108. package/dist/credential/resolver.mjs.map +1 -1
  109. package/dist/credential/vault-store.d.mts +1 -0
  110. package/dist/daemon/config-manager.cjs +279 -0
  111. package/dist/daemon/config-manager.mjs +279 -0
  112. package/dist/daemon/config-manager.mjs.map +1 -0
  113. package/dist/daemon/manager.cjs +164 -0
  114. package/dist/daemon/manager.mjs +157 -0
  115. package/dist/daemon/manager.mjs.map +1 -0
  116. package/dist/daemon/server.cjs +220 -0
  117. package/dist/daemon/server.mjs +220 -0
  118. package/dist/daemon/server.mjs.map +1 -0
  119. package/dist/mcp/http-transport.cjs +14 -1
  120. package/dist/mcp/http-transport.mjs +14 -1
  121. package/dist/mcp/http-transport.mjs.map +1 -1
  122. package/dist/mcp/server.cjs +4 -2
  123. package/dist/mcp/server.mjs +4 -2
  124. package/dist/mcp/server.mjs.map +1 -1
  125. package/dist/mcp/tools.cjs +62 -12
  126. package/dist/mcp/tools.mjs +62 -12
  127. package/dist/mcp/tools.mjs.map +1 -1
  128. package/dist/program/daemon-integration.cjs +46 -0
  129. package/dist/program/daemon-integration.mjs +45 -0
  130. package/dist/program/daemon-integration.mjs.map +1 -0
  131. package/dist/program/program-manager.cjs +162 -0
  132. package/dist/program/program-manager.mjs +162 -0
  133. package/dist/program/program-manager.mjs.map +1 -0
  134. package/dist/program/trigger-scanner.cjs +148 -0
  135. package/dist/program/trigger-scanner.mjs +148 -0
  136. package/dist/program/trigger-scanner.mjs.map +1 -0
  137. package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
  138. package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +11 -0
  139. package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs.map +1 -0
  140. package/dist/providers/vault/dist/encrypted-file.cjs +158 -0
  141. package/dist/providers/vault/dist/encrypted-file.mjs +153 -0
  142. package/dist/providers/vault/dist/encrypted-file.mjs.map +1 -0
  143. package/dist/providers/vault/dist/index.cjs +405 -0
  144. package/dist/providers/vault/dist/index.mjs +400 -0
  145. package/dist/providers/vault/dist/index.mjs.map +1 -0
  146. package/dist/providers/vault/dist/key-resolver.cjs +181 -0
  147. package/dist/providers/vault/dist/key-resolver.mjs +180 -0
  148. package/dist/providers/vault/dist/key-resolver.mjs.map +1 -0
  149. package/dist/repl.cjs +105 -14
  150. package/dist/repl.d.cts.map +1 -1
  151. package/dist/repl.d.mts.map +1 -1
  152. package/dist/repl.mjs +105 -14
  153. package/dist/repl.mjs.map +1 -1
  154. package/package.json +29 -22
@@ -1,4 +1,5 @@
1
1
  import "./cli-auth-context.mjs";
2
2
  import "./mcp-auth-context.mjs";
3
3
  import { CredentialStore, CredentialStoreOptions, Credentials, createCredentialStore } from "./store.mjs";
4
- import "./resolver.mjs";
4
+ import "./resolver.mjs";
5
+ import "./vault-store.mjs";
@@ -15,6 +15,20 @@ let _aigne_afs_utils_schema = require("@aigne/afs/utils/schema");
15
15
  * Requires an MCP Server instance that supports elicitation.
16
16
  */
17
17
  /**
18
+ * Send a logging message to the MCP client.
19
+ * Falls back to console.error if sendLoggingMessage is not available.
20
+ */
21
+ async function logToClient(server, level, message) {
22
+ try {
23
+ await server.sendLoggingMessage({
24
+ level,
25
+ data: message
26
+ });
27
+ } catch {
28
+ console.error(message);
29
+ }
30
+ }
31
+ /**
18
32
  * Create an MCP AuthContext for MCP-based credential collection.
19
33
  */
20
34
  function createMCPAuthContext(options) {
@@ -22,6 +36,7 @@ function createMCPAuthContext(options) {
22
36
  const resolved = options.resolved ?? {};
23
37
  const openURLFn = options.openURL;
24
38
  return {
39
+ nonBlocking: true,
25
40
  get resolved() {
26
41
  return { ...resolved };
27
42
  },
@@ -34,14 +49,14 @@ function createMCPAuthContext(options) {
34
49
  } catch (err) {
35
50
  if (!(err instanceof ElicitationUnsupportedError)) throw err;
36
51
  }
37
- return collectViaBrowser(schema, openURLFn);
52
+ return collectViaBrowser(server, schema, openURLFn);
38
53
  }
39
54
  try {
40
55
  return await collectViaFormMode(server, schema);
41
56
  } catch (err) {
42
57
  if (!(err instanceof ElicitationUnsupportedError)) throw err;
43
58
  }
44
- return collectViaBrowser(schema, openURLFn);
59
+ return collectViaBrowser(server, schema, openURLFn);
45
60
  },
46
61
  async createCallbackServer() {
47
62
  const authServer = await require_auth_server.createAuthServer();
@@ -64,7 +79,13 @@ function createMCPAuthContext(options) {
64
79
  if (result.action === "decline") return "declined";
65
80
  return "cancelled";
66
81
  } catch {
67
- return "cancelled";
82
+ try {
83
+ await logToClient(server, "notice", `${message}\n${url}`);
84
+ await (openURLFn ?? openURL)(url);
85
+ return "accepted";
86
+ } catch {
87
+ return "cancelled";
88
+ }
68
89
  }
69
90
  }
70
91
  };
@@ -147,13 +168,14 @@ async function collectViaURLMode(server, schema) {
147
168
  /**
148
169
  * Fallback: collect credentials by opening a browser directly.
149
170
  * Used when MCP client doesn't support elicitation protocol.
171
+ * Sends a logging notification to the client so the user can see the URL.
150
172
  */
151
- async function collectViaBrowser(schema, openURLFn) {
173
+ async function collectViaBrowser(server, schema, openURLFn) {
152
174
  const properties = schema.properties || {};
153
175
  if (Object.keys(properties).length === 0) return {};
154
176
  const authServer = await require_auth_server.createAuthServer();
155
177
  const formURL = `${authServer.baseURL}/auth?nonce=${authServer.nonce}`;
156
- console.error(`\nPlease fill in credentials in your browser:\n${formURL}`);
178
+ await logToClient(server, "notice", `Please fill in credentials in your browser:\n${formURL}`);
157
179
  try {
158
180
  await (openURLFn ?? openURL)(formURL);
159
181
  } catch {}
@@ -14,6 +14,20 @@ import { getSensitiveFields } from "@aigne/afs/utils/schema";
14
14
  * Requires an MCP Server instance that supports elicitation.
15
15
  */
16
16
  /**
17
+ * Send a logging message to the MCP client.
18
+ * Falls back to console.error if sendLoggingMessage is not available.
19
+ */
20
+ async function logToClient(server, level, message) {
21
+ try {
22
+ await server.sendLoggingMessage({
23
+ level,
24
+ data: message
25
+ });
26
+ } catch {
27
+ console.error(message);
28
+ }
29
+ }
30
+ /**
17
31
  * Create an MCP AuthContext for MCP-based credential collection.
18
32
  */
19
33
  function createMCPAuthContext(options) {
@@ -21,6 +35,7 @@ function createMCPAuthContext(options) {
21
35
  const resolved = options.resolved ?? {};
22
36
  const openURLFn = options.openURL;
23
37
  return {
38
+ nonBlocking: true,
24
39
  get resolved() {
25
40
  return { ...resolved };
26
41
  },
@@ -33,14 +48,14 @@ function createMCPAuthContext(options) {
33
48
  } catch (err) {
34
49
  if (!(err instanceof ElicitationUnsupportedError)) throw err;
35
50
  }
36
- return collectViaBrowser(schema, openURLFn);
51
+ return collectViaBrowser(server, schema, openURLFn);
37
52
  }
38
53
  try {
39
54
  return await collectViaFormMode(server, schema);
40
55
  } catch (err) {
41
56
  if (!(err instanceof ElicitationUnsupportedError)) throw err;
42
57
  }
43
- return collectViaBrowser(schema, openURLFn);
58
+ return collectViaBrowser(server, schema, openURLFn);
44
59
  },
45
60
  async createCallbackServer() {
46
61
  const authServer = await createAuthServer();
@@ -63,7 +78,13 @@ function createMCPAuthContext(options) {
63
78
  if (result.action === "decline") return "declined";
64
79
  return "cancelled";
65
80
  } catch {
66
- return "cancelled";
81
+ try {
82
+ await logToClient(server, "notice", `${message}\n${url}`);
83
+ await (openURLFn ?? openURL)(url);
84
+ return "accepted";
85
+ } catch {
86
+ return "cancelled";
87
+ }
67
88
  }
68
89
  }
69
90
  };
@@ -146,13 +167,14 @@ async function collectViaURLMode(server, schema) {
146
167
  /**
147
168
  * Fallback: collect credentials by opening a browser directly.
148
169
  * Used when MCP client doesn't support elicitation protocol.
170
+ * Sends a logging notification to the client so the user can see the URL.
149
171
  */
150
- async function collectViaBrowser(schema, openURLFn) {
172
+ async function collectViaBrowser(server, schema, openURLFn) {
151
173
  const properties = schema.properties || {};
152
174
  if (Object.keys(properties).length === 0) return {};
153
175
  const authServer = await createAuthServer();
154
176
  const formURL = `${authServer.baseURL}/auth?nonce=${authServer.nonce}`;
155
- console.error(`\nPlease fill in credentials in your browser:\n${formURL}`);
177
+ await logToClient(server, "notice", `Please fill in credentials in your browser:\n${formURL}`);
156
178
  try {
157
179
  await (openURLFn ?? openURL)(formURL);
158
180
  } catch {}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-auth-context.mjs","names":[],"sources":["../../src/credential/mcp-auth-context.ts"],"sourcesContent":["/**\n * MCP AuthContext implementation.\n *\n * Uses MCP elicitation protocol to collect credentials:\n * - Non-sensitive fields: form mode (Client renders UI)\n * - Sensitive fields: URL mode (local HTTP server, data never passes through Client/LLM)\n *\n * Requires an MCP Server instance that supports elicitation.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport type { AuthContext, CallbackServer, JSONSchema7 } from \"@aigne/afs\";\nimport { getSensitiveFields } from \"@aigne/afs/utils/schema\";\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { createAuthServer } from \"./auth-server.js\";\n\nexport interface MCPAuthContextOptions {\n /** The MCP Server instance (for sending elicitation requests) */\n server: Server;\n /** Pre-resolved fields from Step 2 (env/store/config) */\n resolved?: Record<string, unknown>;\n /** Override browser URL opener (for testing). Falls back to platform-default open. */\n openURL?: (url: string) => Promise<void>;\n}\n\n/**\n * Create an MCP AuthContext for MCP-based credential collection.\n */\nexport function createMCPAuthContext(options: MCPAuthContextOptions): AuthContext {\n const { server } = options;\n const resolved = options.resolved ?? {};\n const openURLFn = options.openURL;\n\n return {\n get resolved() {\n return { ...resolved };\n },\n\n async collect(schema: JSONSchema7): Promise<Record<string, unknown> | null> {\n const properties = (schema as any).properties;\n if (!properties || typeof properties !== \"object\") return {};\n\n const sensitiveFields = new Set(getSensitiveFields(schema));\n const hasSensitive = sensitiveFields.size > 0;\n\n // Strategy: try elicitation first, fall back to browser if client doesn't support it.\n // ElicitationUnsupportedError means \"not supported\" → try next mode.\n // null means \"user declined/cancelled\" → stop and return null.\n\n if (hasSensitive) {\n // Sensitive fields: try URL mode (data stays local, never through Client/LLM)\n try {\n return await collectViaURLMode(server, schema);\n } catch (err) {\n if (!(err instanceof ElicitationUnsupportedError)) throw err;\n }\n // URL mode not supported — browser fallback (also keeps data local)\n return collectViaBrowser(schema, openURLFn);\n }\n\n // Non-sensitive only: try form mode (Client renders UI)\n try {\n return await collectViaFormMode(server, schema);\n } catch (err) {\n if (!(err instanceof ElicitationUnsupportedError)) throw err;\n }\n\n // Form mode not supported — browser fallback\n return collectViaBrowser(schema, openURLFn);\n },\n\n async createCallbackServer(): Promise<CallbackServer> {\n const authServer = await createAuthServer();\n return {\n callbackURL: authServer.callbackURL,\n waitForCallback: authServer.waitForCallback.bind(authServer),\n close: authServer.close.bind(authServer),\n };\n },\n\n async requestOpenURL(\n url: string,\n message: string,\n ): Promise<\"accepted\" | \"declined\" | \"cancelled\"> {\n try {\n const elicitationId = randomBytes(16).toString(\"hex\");\n const result = await server.elicitInput({\n mode: \"url\",\n message,\n url,\n elicitationId,\n });\n\n if (result.action === \"accept\") return \"accepted\";\n if (result.action === \"decline\") return \"declined\";\n return \"cancelled\";\n } catch {\n // Client doesn't support elicitation — try direct open\n return \"cancelled\";\n }\n },\n };\n}\n\n/** Sentinel error: elicitation mode not supported by client */\nclass ElicitationUnsupportedError extends Error {\n constructor(mode: string) {\n super(`Client does not support ${mode} elicitation`);\n }\n}\n\n/**\n * Collect non-sensitive fields via MCP form mode elicitation.\n *\n * @throws ElicitationUnsupportedError if client doesn't support form mode\n * @returns collected values, or null if user declined/cancelled\n */\nasync function collectViaFormMode(\n server: Server,\n schema: JSONSchema7,\n): Promise<Record<string, unknown> | null> {\n const properties = (schema as any).properties || {};\n const required = (schema as any).required || [];\n\n // Build elicitation form schema (simplified for MCP form mode)\n const formProperties: Record<string, any> = {};\n for (const [key, prop] of Object.entries(properties) as [string, any][]) {\n formProperties[key] = {\n type: \"string\",\n ...(prop.description ? { description: prop.description } : {}),\n ...(prop.title ? { title: prop.title } : {}),\n ...(prop.default != null ? { default: prop.default } : {}),\n };\n }\n\n try {\n const result = await server.elicitInput({\n mode: \"form\",\n message: \"Please provide the required configuration:\",\n requestedSchema: {\n type: \"object\" as const,\n properties: formProperties,\n required,\n },\n });\n\n if (result.action === \"accept\" && result.content) {\n return result.content as Record<string, unknown>;\n }\n\n // User declined or cancelled\n return null;\n } catch {\n // Client doesn't support form elicitation\n throw new ElicitationUnsupportedError(\"form\");\n }\n}\n\n/**\n * Collect fields containing sensitive data via URL mode.\n * Starts a local HTTP server, sends URL mode elicitation so Client opens the browser.\n * Form data goes directly from browser to local server (never through Client/LLM).\n *\n * @throws ElicitationUnsupportedError if client doesn't support URL mode\n * @returns collected values, or null if user declined/cancelled\n */\nasync function collectViaURLMode(\n server: Server,\n schema: JSONSchema7,\n): Promise<Record<string, unknown> | null> {\n const authServer = await createAuthServer();\n\n try {\n const elicitationId = randomBytes(16).toString(\"hex\");\n const formURL = `${authServer.baseURL}/auth?nonce=${authServer.nonce}`;\n\n // Send URL mode elicitation to client — let errors propagate\n let elicitResult: { action: string };\n try {\n elicitResult = await server.elicitInput({\n mode: \"url\",\n message: \"Please fill in the credential form in your browser:\",\n url: formURL,\n elicitationId,\n });\n } catch {\n // Client doesn't support URL elicitation\n throw new ElicitationUnsupportedError(\"url\");\n }\n\n if (elicitResult.action !== \"accept\") {\n // User declined or cancelled\n return null;\n }\n\n // Client accepted — wait for form submission with timeout\n const result = await authServer.waitForForm(schema as Record<string, any>, {\n title: \"AFS Credential Collection\",\n timeout: 120_000, // 2-minute safety timeout\n });\n\n // Notify client that elicitation is complete\n try {\n const notifyComplete = server.createElicitationCompletionNotifier(elicitationId);\n await notifyComplete();\n } catch {\n // Notification failure is non-critical\n }\n\n return result;\n } finally {\n authServer.close();\n }\n}\n\n/**\n * Fallback: collect credentials by opening a browser directly.\n * Used when MCP client doesn't support elicitation protocol.\n */\nasync function collectViaBrowser(\n schema: JSONSchema7,\n openURLFn?: (url: string) => Promise<void>,\n): Promise<Record<string, unknown> | null> {\n const properties = (schema as any).properties || {};\n if (Object.keys(properties).length === 0) return {};\n\n const authServer = await createAuthServer();\n const formURL = `${authServer.baseURL}/auth?nonce=${authServer.nonce}`;\n\n console.error(`\\nPlease fill in credentials in your browser:\\n${formURL}`);\n\n try {\n await (openURLFn ?? openURL)(formURL);\n } catch {\n // Browser failed to open; URL is already printed above\n }\n\n try {\n const result = await authServer.waitForForm(schema as Record<string, any>, {\n title: \"AFS Credential Collection\",\n });\n return result;\n } finally {\n authServer.close();\n }\n}\n\n/**\n * Open a URL in the default browser.\n */\nfunction openURL(url: string): Promise<void> {\n const cmd =\n process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"cmd\" : \"xdg-open\";\n\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n\n return new Promise((resolve, reject) => {\n execFile(cmd, args, (err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-auth-context.mjs","names":[],"sources":["../../src/credential/mcp-auth-context.ts"],"sourcesContent":["/**\n * MCP AuthContext implementation.\n *\n * Uses MCP elicitation protocol to collect credentials:\n * - Non-sensitive fields: form mode (Client renders UI)\n * - Sensitive fields: URL mode (local HTTP server, data never passes through Client/LLM)\n *\n * Requires an MCP Server instance that supports elicitation.\n */\n\nimport { execFile } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport type { AuthContext, CallbackServer, JSONSchema7 } from \"@aigne/afs\";\nimport { getSensitiveFields } from \"@aigne/afs/utils/schema\";\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { createAuthServer } from \"./auth-server.js\";\n\nexport interface MCPAuthContextOptions {\n /** The MCP Server instance (for sending elicitation requests) */\n server: Server;\n /** Pre-resolved fields from Step 2 (env/store/config) */\n resolved?: Record<string, unknown>;\n /** Override browser URL opener (for testing). Falls back to platform-default open. */\n openURL?: (url: string) => Promise<void>;\n}\n\n/**\n * Send a logging message to the MCP client.\n * Falls back to console.error if sendLoggingMessage is not available.\n */\nasync function logToClient(\n server: Server,\n level: \"info\" | \"notice\" | \"warning\" | \"error\",\n message: string,\n): Promise<void> {\n try {\n await server.sendLoggingMessage({ level, data: message });\n } catch {\n // Logging capability not available — fall back to stderr\n console.error(message);\n }\n}\n\n/**\n * Create an MCP AuthContext for MCP-based credential collection.\n */\nexport function createMCPAuthContext(options: MCPAuthContextOptions): AuthContext {\n const { server } = options;\n const resolved = options.resolved ?? {};\n const openURLFn = options.openURL;\n\n return {\n nonBlocking: true,\n\n get resolved() {\n return { ...resolved };\n },\n\n async collect(schema: JSONSchema7): Promise<Record<string, unknown> | null> {\n const properties = (schema as any).properties;\n if (!properties || typeof properties !== \"object\") return {};\n\n const sensitiveFields = new Set(getSensitiveFields(schema));\n const hasSensitive = sensitiveFields.size > 0;\n\n // Strategy: try elicitation first, fall back to browser if client doesn't support it.\n // ElicitationUnsupportedError means \"not supported\" → try next mode.\n // null means \"user declined/cancelled\" → stop and return null.\n\n if (hasSensitive) {\n // Sensitive fields: try URL mode (data stays local, never through Client/LLM)\n try {\n return await collectViaURLMode(server, schema);\n } catch (err) {\n if (!(err instanceof ElicitationUnsupportedError)) throw err;\n }\n // URL mode not supported — browser fallback (also keeps data local)\n return collectViaBrowser(server, schema, openURLFn);\n }\n\n // Non-sensitive only: try form mode (Client renders UI)\n try {\n return await collectViaFormMode(server, schema);\n } catch (err) {\n if (!(err instanceof ElicitationUnsupportedError)) throw err;\n }\n\n // Form mode not supported — browser fallback\n return collectViaBrowser(server, schema, openURLFn);\n },\n\n async createCallbackServer(): Promise<CallbackServer> {\n const authServer = await createAuthServer();\n return {\n callbackURL: authServer.callbackURL,\n waitForCallback: authServer.waitForCallback.bind(authServer),\n close: authServer.close.bind(authServer),\n };\n },\n\n async requestOpenURL(\n url: string,\n message: string,\n ): Promise<\"accepted\" | \"declined\" | \"cancelled\"> {\n try {\n const elicitationId = randomBytes(16).toString(\"hex\");\n const result = await server.elicitInput({\n mode: \"url\",\n message,\n url,\n elicitationId,\n });\n\n if (result.action === \"accept\") return \"accepted\";\n if (result.action === \"decline\") return \"declined\";\n return \"cancelled\";\n } catch {\n // Client doesn't support elicitation — notify via logging and open browser directly\n try {\n await logToClient(server, \"notice\", `${message}\\n${url}`);\n await (openURLFn ?? openURL)(url);\n return \"accepted\";\n } catch {\n return \"cancelled\";\n }\n }\n },\n };\n}\n\n/** Sentinel error: elicitation mode not supported by client */\nclass ElicitationUnsupportedError extends Error {\n constructor(mode: string) {\n super(`Client does not support ${mode} elicitation`);\n }\n}\n\n/**\n * Collect non-sensitive fields via MCP form mode elicitation.\n *\n * @throws ElicitationUnsupportedError if client doesn't support form mode\n * @returns collected values, or null if user declined/cancelled\n */\nasync function collectViaFormMode(\n server: Server,\n schema: JSONSchema7,\n): Promise<Record<string, unknown> | null> {\n const properties = (schema as any).properties || {};\n const required = (schema as any).required || [];\n\n // Build elicitation form schema (simplified for MCP form mode)\n const formProperties: Record<string, any> = {};\n for (const [key, prop] of Object.entries(properties) as [string, any][]) {\n formProperties[key] = {\n type: \"string\",\n ...(prop.description ? { description: prop.description } : {}),\n ...(prop.title ? { title: prop.title } : {}),\n ...(prop.default != null ? { default: prop.default } : {}),\n };\n }\n\n try {\n const result = await server.elicitInput({\n mode: \"form\",\n message: \"Please provide the required configuration:\",\n requestedSchema: {\n type: \"object\" as const,\n properties: formProperties,\n required,\n },\n });\n\n if (result.action === \"accept\" && result.content) {\n return result.content as Record<string, unknown>;\n }\n\n // User declined or cancelled\n return null;\n } catch {\n // Client doesn't support form elicitation\n throw new ElicitationUnsupportedError(\"form\");\n }\n}\n\n/**\n * Collect fields containing sensitive data via URL mode.\n * Starts a local HTTP server, sends URL mode elicitation so Client opens the browser.\n * Form data goes directly from browser to local server (never through Client/LLM).\n *\n * @throws ElicitationUnsupportedError if client doesn't support URL mode\n * @returns collected values, or null if user declined/cancelled\n */\nasync function collectViaURLMode(\n server: Server,\n schema: JSONSchema7,\n): Promise<Record<string, unknown> | null> {\n const authServer = await createAuthServer();\n\n try {\n const elicitationId = randomBytes(16).toString(\"hex\");\n const formURL = `${authServer.baseURL}/auth?nonce=${authServer.nonce}`;\n\n // Send URL mode elicitation to client — let errors propagate\n let elicitResult: { action: string };\n try {\n elicitResult = await server.elicitInput({\n mode: \"url\",\n message: \"Please fill in the credential form in your browser:\",\n url: formURL,\n elicitationId,\n });\n } catch {\n // Client doesn't support URL elicitation\n throw new ElicitationUnsupportedError(\"url\");\n }\n\n if (elicitResult.action !== \"accept\") {\n // User declined or cancelled\n return null;\n }\n\n // Client accepted — wait for form submission with timeout\n const result = await authServer.waitForForm(schema as Record<string, any>, {\n title: \"AFS Credential Collection\",\n timeout: 120_000, // 2-minute safety timeout\n });\n\n // Notify client that elicitation is complete\n try {\n const notifyComplete = server.createElicitationCompletionNotifier(elicitationId);\n await notifyComplete();\n } catch {\n // Notification failure is non-critical\n }\n\n return result;\n } finally {\n authServer.close();\n }\n}\n\n/**\n * Fallback: collect credentials by opening a browser directly.\n * Used when MCP client doesn't support elicitation protocol.\n * Sends a logging notification to the client so the user can see the URL.\n */\nasync function collectViaBrowser(\n server: Server,\n schema: JSONSchema7,\n openURLFn?: (url: string) => Promise<void>,\n): Promise<Record<string, unknown> | null> {\n const properties = (schema as any).properties || {};\n if (Object.keys(properties).length === 0) return {};\n\n const authServer = await createAuthServer();\n const formURL = `${authServer.baseURL}/auth?nonce=${authServer.nonce}`;\n\n await logToClient(server, \"notice\", `Please fill in credentials in your browser:\\n${formURL}`);\n\n try {\n await (openURLFn ?? openURL)(formURL);\n } catch {\n // Browser failed to open; URL is already sent to client via logging\n }\n\n try {\n const result = await authServer.waitForForm(schema as Record<string, any>, {\n title: \"AFS Credential Collection\",\n });\n return result;\n } finally {\n authServer.close();\n }\n}\n\n/**\n * Open a URL in the default browser.\n */\nfunction openURL(url: string): Promise<void> {\n const cmd =\n process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"cmd\" : \"xdg-open\";\n\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n\n return new Promise((resolve, reject) => {\n execFile(cmd, args, (err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n"],"mappings":"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"}
@@ -56,7 +56,8 @@ async function resolveCredentials(options) {
56
56
  }
57
57
  } catch {}
58
58
  missing = getRequiredMissing(resolved);
59
- if (missing.length === 0 && !options.forceCollect) {
59
+ const allStillMissing = getAllMissing(resolved);
60
+ if (missing.length === 0 && !options.forceCollect && (allStillMissing.length === 0 || !authContext || providerAuth)) {
60
61
  const { sensitive: sensitive$1, nonSensitive: nonSensitive$1 } = (0, _aigne_afs_utils_schema.separateSensitiveValues)(schema, resolved);
61
62
  return {
62
63
  values: { ...resolved },
@@ -79,7 +80,11 @@ async function resolveCredentials(options) {
79
80
  ...authContext,
80
81
  get resolved() {
81
82
  return { ...resolved };
82
- }
83
+ },
84
+ persistCredentials: credentialStore ? async (creds) => {
85
+ const { sensitive: bgSensitive } = (0, _aigne_afs_utils_schema.separateSensitiveValues)(schema, creds);
86
+ if (Object.keys(bgSensitive).length > 0) await credentialStore.set(mount.uri, bgSensitive);
87
+ } : void 0
83
88
  });
84
89
  else {
85
90
  const fieldsForForm = allFields.filter((f) => known[f] === void 0);
@@ -55,7 +55,8 @@ async function resolveCredentials(options) {
55
55
  }
56
56
  } catch {}
57
57
  missing = getRequiredMissing(resolved);
58
- if (missing.length === 0 && !options.forceCollect) {
58
+ const allStillMissing = getAllMissing(resolved);
59
+ if (missing.length === 0 && !options.forceCollect && (allStillMissing.length === 0 || !authContext || providerAuth)) {
59
60
  const { sensitive: sensitive$1, nonSensitive: nonSensitive$1 } = separateSensitiveValues(schema, resolved);
60
61
  return {
61
62
  values: { ...resolved },
@@ -78,7 +79,11 @@ async function resolveCredentials(options) {
78
79
  ...authContext,
79
80
  get resolved() {
80
81
  return { ...resolved };
81
- }
82
+ },
83
+ persistCredentials: credentialStore ? async (creds) => {
84
+ const { sensitive: bgSensitive } = separateSensitiveValues(schema, creds);
85
+ if (Object.keys(bgSensitive).length > 0) await credentialStore.set(mount.uri, bgSensitive);
86
+ } : void 0
82
87
  });
83
88
  else {
84
89
  const fieldsForForm = allFields.filter((f) => known[f] === void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.mjs","names":["resolved"],"sources":["../../src/credential/resolver.ts"],"sourcesContent":["/**\n * 4-Step Credential Resolution Flow\n *\n * Step 1: Determine missing fields (from provider schema vs known values)\n * Step 2: Silent resolution (config > env > credential store)\n * Step 3: Interactive collection (provider auth() or default collect())\n * Step 4: Unified persistence (sensitive → credentials.toml, non-sensitive → config options)\n */\n\nimport type { AuthContext, JSONSchema7, MountConfig } from \"@aigne/afs\";\nimport { resolveEnvFromSchema, separateSensitiveValues } from \"@aigne/afs/utils/schema\";\nimport type { CredentialStore } from \"./store.js\";\n\nexport interface ResolveCredentialsOptions {\n /** Mount configuration */\n mount: MountConfig;\n /** JSON Schema for this provider (from z.toJSONSchema() or manifest) */\n schema: JSONSchema7;\n /** Auth context for interactive collection (CLI or MCP) */\n authContext?: AuthContext;\n /** Credential store for reading/writing credentials */\n credentialStore?: CredentialStore;\n /** Provider class with optional auth() method */\n providerAuth?: (context: AuthContext) => Promise<Record<string, unknown> | null>;\n /** Environment variables (defaults to process.env) */\n env?: Record<string, string | undefined>;\n /** Force interactive collection even when all fields are resolved silently.\n * Used for retry after health-check failure with stale env/store values. */\n forceCollect?: boolean;\n}\n\nexport interface ResolveCredentialsResult {\n /** All resolved values (merged from all sources) */\n values: Record<string, unknown>;\n /** Values to persist as sensitive credentials */\n sensitive: Record<string, string>;\n /** Values to persist as non-sensitive config options */\n nonSensitive: Record<string, unknown>;\n /** Whether any interactive collection was needed */\n collected: boolean;\n}\n\n/**\n * Execute the 4-step credential resolution flow.\n *\n * Returns all resolved values plus their split into sensitive/non-sensitive\n * for persistence. The caller is responsible for actual persistence.\n *\n * @returns null if user declined/cancelled collection\n */\nexport async function resolveCredentials(\n options: ResolveCredentialsOptions,\n): Promise<ResolveCredentialsResult | null> {\n const { mount, schema, authContext, credentialStore, providerAuth, env = process.env } = options;\n\n const properties = (schema as any).properties;\n if (!properties || typeof properties !== \"object\" || Object.keys(properties).length === 0) {\n // No fields to resolve\n return { values: {}, sensitive: {}, nonSensitive: {}, collected: false };\n }\n\n const allFields = Object.keys(properties);\n const requiredFields = ((schema as any).required ?? []) as string[];\n\n // Only required fields without a schema default truly block silent resolution —\n // optional fields and fields with defaults should not trigger interactive auth.\n const requiredWithoutDefault = requiredFields.filter(\n (f) => properties[f] && properties[f].default === undefined,\n );\n\n // ─── Step 1: Determine known values from mount config ─────────────────\n const known: Record<string, unknown> = {};\n\n // Extract values from mount.auth, mount.token, mount.options\n if (mount.auth !== undefined) known.auth = mount.auth;\n if (mount.token !== undefined) known.token = mount.token;\n if (mount.options) {\n for (const [k, v] of Object.entries(mount.options)) {\n if (v !== undefined) known[k] = v;\n }\n }\n\n // Check which fields are still missing — allFields for Step 1 optimization,\n // requiredWithoutDefault for the Step 2→3 decision on interactive auth.\n const getAllMissing = (resolved: Record<string, unknown>) =>\n allFields.filter((f) => resolved[f] === undefined);\n const getRequiredMissing = (resolved: Record<string, unknown>) =>\n requiredWithoutDefault.filter((f) => resolved[f] === undefined);\n\n let missing = getAllMissing(known);\n if (missing.length === 0) {\n // All fields already provided via config — short circuit\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, known);\n return { values: { ...known }, sensitive, nonSensitive, collected: false };\n }\n\n // ─── Step 2: Silent resolution ────────────────────────────────────────\n const resolved: Record<string, unknown> = { ...known };\n\n // 2a. Environment variables from schema env declarations\n const envResolved = resolveEnvFromSchema(schema, env as Record<string, string | undefined>);\n for (const [field, value] of Object.entries(envResolved)) {\n if (resolved[field] === undefined) {\n resolved[field] = value;\n }\n }\n\n // 2b. Credential store — keyed by URI (credentials belong to the resource, not the path)\n const storeResolved: Record<string, unknown> = {};\n if (credentialStore) {\n try {\n const stored = await credentialStore.get(mount.uri);\n if (stored) {\n for (const [field, value] of Object.entries(stored)) {\n if (field.startsWith(\"env:\")) {\n // Reconstruct env Record from flattened env:KEY credential entries\n if (resolved.env === undefined) resolved.env = {};\n (resolved.env as Record<string, string>)[field.slice(4)] = value;\n if (storeResolved.env === undefined) storeResolved.env = {};\n (storeResolved.env as Record<string, string>)[field.slice(4)] = value;\n } else {\n if (resolved[field] === undefined) {\n resolved[field] = value;\n }\n storeResolved[field] = value;\n }\n }\n }\n } catch {\n // Credential store read failure is non-fatal\n }\n }\n\n missing = getRequiredMissing(resolved);\n if (missing.length === 0 && !options.forceCollect) {\n // All fields resolved silently\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, resolved);\n return { values: { ...resolved }, sensitive, nonSensitive, collected: false };\n }\n\n // ─── Step 3: Interactive collection ───────────────────────────────────\n if (!authContext) {\n // No auth context available — can't collect. Return what we have.\n // Caller should attempt mount with partial values and let the provider error if needed.\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, resolved);\n return { values: { ...resolved }, sensitive, nonSensitive, collected: false };\n }\n\n let collected: Record<string, unknown> | null = null;\n\n if (providerAuth) {\n // Provider has custom auth() — delegate to it\n collected = await providerAuth({\n ...authContext,\n get resolved() {\n return { ...resolved };\n },\n });\n } else {\n // Show all fields not explicitly provided via config/CLI args.\n // Env and store values are pre-filled as defaults so the user can verify/override.\n const fieldsForForm = allFields.filter((f) => known[f] === undefined);\n const defaults: Record<string, unknown> = { ...envResolved };\n for (const [f, v] of Object.entries(storeResolved)) {\n if (defaults[f] === undefined) defaults[f] = v;\n }\n const missingSchema = buildMissingFieldsSchema(schema, fieldsForForm, defaults);\n collected = await authContext.collect(missingSchema);\n }\n\n if (collected === null) {\n // User declined/cancelled\n return null;\n }\n\n // Merge collected values\n for (const [field, value] of Object.entries(collected)) {\n if (value !== undefined) {\n resolved[field] = value;\n }\n }\n\n // ─── Step 4: Split for persistence ────────────────────────────────────\n // Only split the newly collected values (not the ones from config/env/store)\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, collected);\n\n return { values: { ...resolved }, sensitive, nonSensitive, collected: true };\n}\n\n/**\n * Build a JSON Schema containing only the specified fields from the original schema.\n * Fields with env-resolved values get a `default` so the collection form can pre-fill them.\n */\nfunction buildMissingFieldsSchema(\n schema: JSONSchema7,\n fields: string[],\n envDefaults?: Record<string, unknown>,\n): JSONSchema7 {\n const properties = (schema as any).properties ?? {};\n const required = ((schema as any).required ?? []) as string[];\n\n const fieldProperties: Record<string, unknown> = {};\n const fieldRequired: string[] = [];\n\n for (const field of fields) {\n if (properties[field]) {\n let prop = properties[field];\n // Pre-fill with env value so user can see and override\n if (envDefaults?.[field] !== undefined && prop.default === undefined) {\n prop = { ...prop, default: envDefaults[field] };\n }\n fieldProperties[field] = prop;\n if (required.includes(field)) {\n fieldRequired.push(field);\n }\n }\n }\n\n return {\n type: \"object\",\n properties: fieldProperties,\n ...(fieldRequired.length > 0 ? { required: fieldRequired } : {}),\n } as JSONSchema7;\n}\n"],"mappings":";;;;;;;;;;;AAkDA,eAAsB,mBACpB,SAC0C;CAC1C,MAAM,EAAE,OAAO,QAAQ,aAAa,iBAAiB,cAAc,MAAM,QAAQ,QAAQ;CAEzF,MAAM,aAAc,OAAe;AACnC,KAAI,CAAC,cAAc,OAAO,eAAe,YAAY,OAAO,KAAK,WAAW,CAAC,WAAW,EAEtF,QAAO;EAAE,QAAQ,EAAE;EAAE,WAAW,EAAE;EAAE,cAAc,EAAE;EAAE,WAAW;EAAO;CAG1E,MAAM,YAAY,OAAO,KAAK,WAAW;CAKzC,MAAM,0BAJmB,OAAe,YAAY,EAAE,EAIR,QAC3C,MAAM,WAAW,MAAM,WAAW,GAAG,YAAY,OACnD;CAGD,MAAM,QAAiC,EAAE;AAGzC,KAAI,MAAM,SAAS,OAAW,OAAM,OAAO,MAAM;AACjD,KAAI,MAAM,UAAU,OAAW,OAAM,QAAQ,MAAM;AACnD,KAAI,MAAM,SACR;OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,MAAM,QAAQ,CAChD,KAAI,MAAM,OAAW,OAAM,KAAK;;CAMpC,MAAM,iBAAiB,eACrB,UAAU,QAAQ,MAAMA,WAAS,OAAO,OAAU;CACpD,MAAM,sBAAsB,eAC1B,uBAAuB,QAAQ,MAAMA,WAAS,OAAO,OAAU;CAEjE,IAAI,UAAU,cAAc,MAAM;AAClC,KAAI,QAAQ,WAAW,GAAG;EAExB,MAAM,EAAE,wBAAW,iCAAiB,wBAAwB,QAAQ,MAAM;AAC1E,SAAO;GAAE,QAAQ,EAAE,GAAG,OAAO;GAAE;GAAW;GAAc,WAAW;GAAO;;CAI5E,MAAM,WAAoC,EAAE,GAAG,OAAO;CAGtD,MAAM,cAAc,qBAAqB,QAAQ,IAA0C;AAC3F,MAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,YAAY,CACtD,KAAI,SAAS,WAAW,OACtB,UAAS,SAAS;CAKtB,MAAM,gBAAyC,EAAE;AACjD,KAAI,gBACF,KAAI;EACF,MAAM,SAAS,MAAM,gBAAgB,IAAI,MAAM,IAAI;AACnD,MAAI,OACF,MAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,OAAO,CACjD,KAAI,MAAM,WAAW,OAAO,EAAE;AAE5B,OAAI,SAAS,QAAQ,OAAW,UAAS,MAAM,EAAE;AACjD,GAAC,SAAS,IAA+B,MAAM,MAAM,EAAE,IAAI;AAC3D,OAAI,cAAc,QAAQ,OAAW,eAAc,MAAM,EAAE;AAC3D,GAAC,cAAc,IAA+B,MAAM,MAAM,EAAE,IAAI;SAC3D;AACL,OAAI,SAAS,WAAW,OACtB,UAAS,SAAS;AAEpB,iBAAc,SAAS;;SAIvB;AAKV,WAAU,mBAAmB,SAAS;AACtC,KAAI,QAAQ,WAAW,KAAK,CAAC,QAAQ,cAAc;EAEjD,MAAM,EAAE,wBAAW,iCAAiB,wBAAwB,QAAQ,SAAS;AAC7E,SAAO;GAAE,QAAQ,EAAE,GAAG,UAAU;GAAE;GAAW;GAAc,WAAW;GAAO;;AAI/E,KAAI,CAAC,aAAa;EAGhB,MAAM,EAAE,wBAAW,iCAAiB,wBAAwB,QAAQ,SAAS;AAC7E,SAAO;GAAE,QAAQ,EAAE,GAAG,UAAU;GAAE;GAAW;GAAc,WAAW;GAAO;;CAG/E,IAAI,YAA4C;AAEhD,KAAI,aAEF,aAAY,MAAM,aAAa;EAC7B,GAAG;EACH,IAAI,WAAW;AACb,UAAO,EAAE,GAAG,UAAU;;EAEzB,CAAC;MACG;EAGL,MAAM,gBAAgB,UAAU,QAAQ,MAAM,MAAM,OAAO,OAAU;EACrE,MAAM,WAAoC,EAAE,GAAG,aAAa;AAC5D,OAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,cAAc,CAChD,KAAI,SAAS,OAAO,OAAW,UAAS,KAAK;EAE/C,MAAM,gBAAgB,yBAAyB,QAAQ,eAAe,SAAS;AAC/E,cAAY,MAAM,YAAY,QAAQ,cAAc;;AAGtD,KAAI,cAAc,KAEhB,QAAO;AAIT,MAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,UAAU,CACpD,KAAI,UAAU,OACZ,UAAS,SAAS;CAMtB,MAAM,EAAE,WAAW,iBAAiB,wBAAwB,QAAQ,UAAU;AAE9E,QAAO;EAAE,QAAQ,EAAE,GAAG,UAAU;EAAE;EAAW;EAAc,WAAW;EAAM;;;;;;AAO9E,SAAS,yBACP,QACA,QACA,aACa;CACb,MAAM,aAAc,OAAe,cAAc,EAAE;CACnD,MAAM,WAAa,OAAe,YAAY,EAAE;CAEhD,MAAM,kBAA2C,EAAE;CACnD,MAAM,gBAA0B,EAAE;AAElC,MAAK,MAAM,SAAS,OAClB,KAAI,WAAW,QAAQ;EACrB,IAAI,OAAO,WAAW;AAEtB,MAAI,cAAc,WAAW,UAAa,KAAK,YAAY,OACzD,QAAO;GAAE,GAAG;GAAM,SAAS,YAAY;GAAQ;AAEjD,kBAAgB,SAAS;AACzB,MAAI,SAAS,SAAS,MAAM,CAC1B,eAAc,KAAK,MAAM;;AAK/B,QAAO;EACL,MAAM;EACN,YAAY;EACZ,GAAI,cAAc,SAAS,IAAI,EAAE,UAAU,eAAe,GAAG,EAAE;EAChE"}
1
+ {"version":3,"file":"resolver.mjs","names":["resolved"],"sources":["../../src/credential/resolver.ts"],"sourcesContent":["/**\n * 4-Step Credential Resolution Flow\n *\n * Step 1: Determine missing fields (from provider schema vs known values)\n * Step 2: Silent resolution (config > env > credential store)\n * Step 3: Interactive collection (provider auth() or default collect())\n * Step 4: Unified persistence (sensitive → credentials.toml, non-sensitive → config options)\n */\n\nimport type { AuthContext, JSONSchema7, MountConfig } from \"@aigne/afs\";\nimport { resolveEnvFromSchema, separateSensitiveValues } from \"@aigne/afs/utils/schema\";\nimport type { CredentialStore } from \"./store.js\";\n\nexport interface ResolveCredentialsOptions {\n /** Mount configuration */\n mount: MountConfig;\n /** JSON Schema for this provider (from z.toJSONSchema() or manifest) */\n schema: JSONSchema7;\n /** Auth context for interactive collection (CLI or MCP) */\n authContext?: AuthContext;\n /** Credential store for reading/writing credentials */\n credentialStore?: CredentialStore;\n /** Provider class with optional auth() method */\n providerAuth?: (context: AuthContext) => Promise<Record<string, unknown> | null>;\n /** Environment variables (defaults to process.env) */\n env?: Record<string, string | undefined>;\n /** Force interactive collection even when all fields are resolved silently.\n * Used for retry after health-check failure with stale env/store values. */\n forceCollect?: boolean;\n}\n\nexport interface ResolveCredentialsResult {\n /** All resolved values (merged from all sources) */\n values: Record<string, unknown>;\n /** Values to persist as sensitive credentials */\n sensitive: Record<string, string>;\n /** Values to persist as non-sensitive config options */\n nonSensitive: Record<string, unknown>;\n /** Whether any interactive collection was needed */\n collected: boolean;\n}\n\n/**\n * Execute the 4-step credential resolution flow.\n *\n * Returns all resolved values plus their split into sensitive/non-sensitive\n * for persistence. The caller is responsible for actual persistence.\n *\n * @returns null if user declined/cancelled collection\n */\nexport async function resolveCredentials(\n options: ResolveCredentialsOptions,\n): Promise<ResolveCredentialsResult | null> {\n const { mount, schema, authContext, credentialStore, providerAuth, env = process.env } = options;\n\n const properties = (schema as any).properties;\n if (!properties || typeof properties !== \"object\" || Object.keys(properties).length === 0) {\n // No fields to resolve\n return { values: {}, sensitive: {}, nonSensitive: {}, collected: false };\n }\n\n const allFields = Object.keys(properties);\n const requiredFields = ((schema as any).required ?? []) as string[];\n\n // Only required fields without a schema default truly block silent resolution —\n // optional fields and fields with defaults should not trigger interactive auth.\n const requiredWithoutDefault = requiredFields.filter(\n (f) => properties[f] && properties[f].default === undefined,\n );\n\n // ─── Step 1: Determine known values from mount config ─────────────────\n const known: Record<string, unknown> = {};\n\n // Extract values from mount.auth, mount.token, mount.options\n if (mount.auth !== undefined) known.auth = mount.auth;\n if (mount.token !== undefined) known.token = mount.token;\n if (mount.options) {\n for (const [k, v] of Object.entries(mount.options)) {\n if (v !== undefined) known[k] = v;\n }\n }\n\n // Check which fields are still missing — allFields for Step 1 optimization,\n // requiredWithoutDefault for the Step 2→3 decision on interactive auth.\n const getAllMissing = (resolved: Record<string, unknown>) =>\n allFields.filter((f) => resolved[f] === undefined);\n const getRequiredMissing = (resolved: Record<string, unknown>) =>\n requiredWithoutDefault.filter((f) => resolved[f] === undefined);\n\n let missing = getAllMissing(known);\n if (missing.length === 0) {\n // All fields already provided via config — short circuit\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, known);\n return { values: { ...known }, sensitive, nonSensitive, collected: false };\n }\n\n // ─── Step 2: Silent resolution ────────────────────────────────────────\n const resolved: Record<string, unknown> = { ...known };\n\n // 2a. Environment variables from schema env declarations\n const envResolved = resolveEnvFromSchema(schema, env as Record<string, string | undefined>);\n for (const [field, value] of Object.entries(envResolved)) {\n if (resolved[field] === undefined) {\n resolved[field] = value;\n }\n }\n\n // 2b. Credential store — keyed by URI (credentials belong to the resource, not the path)\n const storeResolved: Record<string, unknown> = {};\n if (credentialStore) {\n try {\n const stored = await credentialStore.get(mount.uri);\n if (stored) {\n for (const [field, value] of Object.entries(stored)) {\n if (field.startsWith(\"env:\")) {\n // Reconstruct env Record from flattened env:KEY credential entries\n if (resolved.env === undefined) resolved.env = {};\n (resolved.env as Record<string, string>)[field.slice(4)] = value;\n if (storeResolved.env === undefined) storeResolved.env = {};\n (storeResolved.env as Record<string, string>)[field.slice(4)] = value;\n } else {\n if (resolved[field] === undefined) {\n resolved[field] = value;\n }\n storeResolved[field] = value;\n }\n }\n }\n } catch {\n // Credential store read failure is non-fatal\n }\n }\n\n missing = getRequiredMissing(resolved);\n const allStillMissing = getAllMissing(resolved);\n if (\n missing.length === 0 &&\n !options.forceCollect &&\n (allStillMissing.length === 0 || !authContext || providerAuth)\n ) {\n // All required fields resolved silently, and either:\n // - all fields resolved, or\n // - no auth context to collect interactively, or\n // - provider has custom auth (OAuth etc.) — don't trigger heavy auth flow for optional fields\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, resolved);\n return { values: { ...resolved }, sensitive, nonSensitive, collected: false };\n }\n\n // ─── Step 3: Interactive collection ───────────────────────────────────\n if (!authContext) {\n // No auth context available — can't collect. Return what we have.\n // Caller should attempt mount with partial values and let the provider error if needed.\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, resolved);\n return { values: { ...resolved }, sensitive, nonSensitive, collected: false };\n }\n\n let collected: Record<string, unknown> | null = null;\n\n if (providerAuth) {\n // Provider has custom auth() — delegate to it.\n // Inject persistCredentials so non-blocking auth flows can store\n // credentials in the background (e.g., MCP browser auth).\n collected = await providerAuth({\n ...authContext,\n get resolved() {\n return { ...resolved };\n },\n persistCredentials: credentialStore\n ? async (creds: Record<string, unknown>) => {\n const { sensitive: bgSensitive } = separateSensitiveValues(schema, creds);\n if (Object.keys(bgSensitive).length > 0) {\n await credentialStore.set(mount.uri, bgSensitive);\n }\n }\n : undefined,\n });\n } else {\n // Show all fields not explicitly provided via config/CLI args.\n // Env and store values are pre-filled as defaults so the user can verify/override.\n const fieldsForForm = allFields.filter((f) => known[f] === undefined);\n const defaults: Record<string, unknown> = { ...envResolved };\n for (const [f, v] of Object.entries(storeResolved)) {\n if (defaults[f] === undefined) defaults[f] = v;\n }\n const missingSchema = buildMissingFieldsSchema(schema, fieldsForForm, defaults);\n collected = await authContext.collect(missingSchema);\n }\n\n if (collected === null) {\n // User declined/cancelled\n return null;\n }\n\n // Merge collected values\n for (const [field, value] of Object.entries(collected)) {\n if (value !== undefined) {\n resolved[field] = value;\n }\n }\n\n // ─── Step 4: Split for persistence ────────────────────────────────────\n // Only split the newly collected values (not the ones from config/env/store)\n const { sensitive, nonSensitive } = separateSensitiveValues(schema, collected);\n\n return { values: { ...resolved }, sensitive, nonSensitive, collected: true };\n}\n\n/**\n * Build a JSON Schema containing only the specified fields from the original schema.\n * Fields with env-resolved values get a `default` so the collection form can pre-fill them.\n */\nfunction buildMissingFieldsSchema(\n schema: JSONSchema7,\n fields: string[],\n envDefaults?: Record<string, unknown>,\n): JSONSchema7 {\n const properties = (schema as any).properties ?? {};\n const required = ((schema as any).required ?? []) as string[];\n\n const fieldProperties: Record<string, unknown> = {};\n const fieldRequired: string[] = [];\n\n for (const field of fields) {\n if (properties[field]) {\n let prop = properties[field];\n // Pre-fill with env value so user can see and override\n if (envDefaults?.[field] !== undefined && prop.default === undefined) {\n prop = { ...prop, default: envDefaults[field] };\n }\n fieldProperties[field] = prop;\n if (required.includes(field)) {\n fieldRequired.push(field);\n }\n }\n }\n\n return {\n type: \"object\",\n properties: fieldProperties,\n ...(fieldRequired.length > 0 ? { required: fieldRequired } : {}),\n } as JSONSchema7;\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ import { AFSModule } from "@aigne/afs";
@@ -0,0 +1,279 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ const require_loader = require('../config/loader.cjs');
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_path = require("node:path");
5
+ let smol_toml = require("smol-toml");
6
+ let _aigne_afs = require("@aigne/afs");
7
+ let node_fs = require("node:fs");
8
+
9
+ //#region src/daemon/config-manager.ts
10
+ /**
11
+ * ConfigManager Implementation
12
+ *
13
+ * Manages config.toml read/write operations for the daemon.
14
+ * Implements the ConfigManager interface from @aigne/afs-explorer.
15
+ */
16
+ var DaemonConfigManager = class {
17
+ cwd;
18
+ afs;
19
+ onConfigChanged;
20
+ watcher;
21
+ _selfWriteTimestamp = 0;
22
+ _debounceTimer;
23
+ _failures = [];
24
+ /** Tracks mount paths that originated from config.toml. Only these are eligible for unmount on reload. */
25
+ configManagedPaths;
26
+ registry;
27
+ constructor(options) {
28
+ this.cwd = options.cwd;
29
+ this.afs = options.afs;
30
+ this.registry = options.registry;
31
+ this.onConfigChanged = options.onConfigChanged;
32
+ this.configManagedPaths = new Set(options.configMountPaths ?? []);
33
+ if (options.failures) this._failures = [...options.failures];
34
+ }
35
+ async getConfig() {
36
+ return new require_loader.ConfigLoader().load(this.cwd);
37
+ }
38
+ async getMountList() {
39
+ const mounts = this.afs.getMounts();
40
+ return {
41
+ mounts: await Promise.all(mounts.map(async (m) => {
42
+ const ctor = m.module.constructor;
43
+ const manifest = typeof ctor.manifest === "function" ? ctor.manifest() : null;
44
+ let url;
45
+ try {
46
+ if (typeof m.module.stat === "function") url = (await m.module.stat("/", {}))?.data?.meta?.url || void 0;
47
+ } catch {}
48
+ return {
49
+ namespace: m.namespace,
50
+ path: m.path,
51
+ name: m.module.name,
52
+ description: m.module.description,
53
+ accessMode: m.module.accessMode,
54
+ category: manifest?.category || void 0,
55
+ tags: manifest?.tags || void 0,
56
+ uri: manifest?.uriTemplate || void 0,
57
+ url
58
+ };
59
+ })),
60
+ failures: this._failures
61
+ };
62
+ }
63
+ async addMount(mount) {
64
+ const uri = mount.uri;
65
+ const path = mount.path;
66
+ if (!uri || !path) throw new Error("uri and path are required");
67
+ const mountConfig = await this.buildMountWithCredentials(uri, path, mount.auth, {
68
+ description: mount.description,
69
+ access_mode: mount.accessMode,
70
+ namespace: mount.namespace,
71
+ options: mount.options
72
+ });
73
+ const provider = await (this.registry ?? new _aigne_afs.ProviderRegistry()).createProvider(mountConfig);
74
+ await this.afs.mount(provider, path, { namespace: mountConfig.namespace ?? null });
75
+ await this.persistAddMount(mountConfig);
76
+ this.configManagedPaths.add(path);
77
+ }
78
+ async removeMount(path) {
79
+ this.afs.unmount(path);
80
+ await this.persistRemoveMount(path);
81
+ this.configManagedPaths.delete(path);
82
+ }
83
+ async updateMount(path, updates) {
84
+ const config = await this.readConfigFile();
85
+ const entry = (config.mounts ?? []).find((m) => m.path === path);
86
+ if (!entry) throw new Error(`Mount "${path}" not found in config`);
87
+ if (updates.description !== void 0) entry.description = updates.description;
88
+ if (updates.accessMode !== void 0) entry.access_mode = updates.accessMode;
89
+ if (updates.auth !== void 0) entry.auth = updates.auth;
90
+ await this.writeConfigFile(config);
91
+ try {
92
+ this.afs.unmount(path);
93
+ } catch {}
94
+ const mount = await this.buildMountWithCredentials(entry.uri, entry.path, entry.auth, {
95
+ description: entry.description,
96
+ access_mode: entry.access_mode,
97
+ namespace: entry.namespace,
98
+ options: entry.options
99
+ });
100
+ const provider = await new _aigne_afs.ProviderRegistry().createProvider(mount);
101
+ await this.afs.mount(provider, path, { namespace: entry.namespace ?? null });
102
+ }
103
+ async testMount(uri, auth) {
104
+ try {
105
+ const registry = new _aigne_afs.ProviderRegistry();
106
+ const mount = await this.buildMountWithCredentials(uri, "/test", auth);
107
+ const provider = await registry.createProvider(mount);
108
+ await provider.list("/", {});
109
+ const providerName = provider.name;
110
+ try {
111
+ await provider.close?.();
112
+ } catch {}
113
+ return {
114
+ success: true,
115
+ providerName
116
+ };
117
+ } catch (err) {
118
+ return {
119
+ success: false,
120
+ error: err instanceof Error ? err.message : String(err)
121
+ };
122
+ }
123
+ }
124
+ async reloadConfig() {
125
+ const newConfig = await new require_loader.ConfigLoader().load(this.cwd);
126
+ const currentMounts = new Set(this.afs.getMounts().map((m) => m.path));
127
+ const newMountPaths = new Set(newConfig.mounts.map((m) => m.path));
128
+ const removed = [];
129
+ for (const path of this.configManagedPaths) if (!newMountPaths.has(path)) {
130
+ try {
131
+ this.afs.unmount(path);
132
+ removed.push(path);
133
+ } catch {}
134
+ this.configManagedPaths.delete(path);
135
+ }
136
+ const added = [];
137
+ const registry = new _aigne_afs.ProviderRegistry();
138
+ for (const mount of newConfig.mounts) if (!currentMounts.has(mount.path)) try {
139
+ const provider = await registry.createProvider(mount);
140
+ await this.afs.mount(provider, mount.path, { namespace: mount.namespace ?? null });
141
+ added.push(mount.path);
142
+ this.configManagedPaths.add(mount.path);
143
+ } catch (err) {
144
+ console.warn(`[reload] Failed to mount ${mount.path}: ${err instanceof Error ? err.message : String(err)}`);
145
+ }
146
+ if (this.onConfigChanged && (added.length > 0 || removed.length > 0)) this.onConfigChanged(added, removed);
147
+ }
148
+ /**
149
+ * Start watching config file for external changes.
150
+ */
151
+ startWatching() {
152
+ const configPath = this.getConfigPath();
153
+ try {
154
+ this.watcher = (0, node_fs.watch)((0, node_path.dirname)(configPath), (_event, filename) => {
155
+ if (filename !== "config.toml") return;
156
+ if (Date.now() - this._selfWriteTimestamp < 500) return;
157
+ if (this._debounceTimer) clearTimeout(this._debounceTimer);
158
+ this._debounceTimer = setTimeout(() => {
159
+ this.reloadConfig().catch((err) => {
160
+ console.warn(`[watch] Reload failed: ${err instanceof Error ? err.message : String(err)}`);
161
+ });
162
+ }, 300);
163
+ });
164
+ } catch {}
165
+ }
166
+ async getRegistry() {
167
+ try {
168
+ const { scanInstalledProviders } = await import("@aigne/afs-registry");
169
+ return (await scanInstalledProviders()).map((m) => ({
170
+ name: m.name,
171
+ description: m.description,
172
+ category: m.category,
173
+ uriTemplate: m.uriTemplate,
174
+ tags: m.tags
175
+ }));
176
+ } catch {
177
+ return [];
178
+ }
179
+ }
180
+ stopWatching() {
181
+ if (this.watcher) {
182
+ this.watcher.close();
183
+ this.watcher = void 0;
184
+ }
185
+ if (this._debounceTimer) {
186
+ clearTimeout(this._debounceTimer);
187
+ this._debounceTimer = void 0;
188
+ }
189
+ }
190
+ /**
191
+ * Build a MountConfig with proper credential mapping.
192
+ *
193
+ * Uses the provider schema to:
194
+ * 1. Resolve credentials from env vars (e.g. AIGNE_HUB_API_KEY)
195
+ * 2. Resolve credentials from the credential store (vault)
196
+ * 3. Map generic `auth` to the provider's specific credential field (e.g. apiKey)
197
+ */
198
+ async buildMountWithCredentials(uri, path, auth, extra) {
199
+ const mount = {
200
+ uri,
201
+ path,
202
+ auth,
203
+ description: extra?.description,
204
+ access_mode: extra?.access_mode,
205
+ namespace: extra?.namespace,
206
+ options: extra?.options ? { ...extra.options } : void 0
207
+ };
208
+ try {
209
+ const info = await new _aigne_afs.ProviderRegistry().getProviderInfo(uri);
210
+ if (!info?.schema) return mount;
211
+ const { getSensitiveFields, resolveEnvFromSchema } = await import("@aigne/afs/utils/schema");
212
+ const opts = mount.options ?? {};
213
+ const envResolved = resolveEnvFromSchema(info.schema);
214
+ for (const [field, value] of Object.entries(envResolved)) if (opts[field] === void 0) opts[field] = value;
215
+ try {
216
+ const { createCredentialStore } = await Promise.resolve().then(() => require("../credential/store.cjs"));
217
+ const stored = await createCredentialStore().get(uri);
218
+ if (stored) {
219
+ for (const [field, value] of Object.entries(stored)) if (!field.startsWith("env:") && opts[field] === void 0) opts[field] = value;
220
+ }
221
+ } catch {}
222
+ if (auth) {
223
+ const sensitiveFields = getSensitiveFields(info.schema);
224
+ for (const field of sensitiveFields) if (field !== "auth" && field !== "token" && opts[field] === void 0) {
225
+ opts[field] = auth;
226
+ break;
227
+ }
228
+ }
229
+ if (Object.keys(opts).length > 0) mount.options = opts;
230
+ } catch {}
231
+ return mount;
232
+ }
233
+ getConfigPath() {
234
+ return (0, node_path.join)(this.cwd, ".afs-config", "config.toml");
235
+ }
236
+ async readConfigFile() {
237
+ const configPath = this.getConfigPath();
238
+ try {
239
+ return (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8"));
240
+ } catch {
241
+ return { mounts: [] };
242
+ }
243
+ }
244
+ async writeConfigFile(config) {
245
+ const configPath = this.getConfigPath();
246
+ const configDir = (0, node_path.dirname)(configPath);
247
+ try {
248
+ await (0, node_fs_promises.mkdir)(configDir, { recursive: true });
249
+ } catch {}
250
+ this._selfWriteTimestamp = Date.now();
251
+ await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(config), "utf-8");
252
+ }
253
+ async persistAddMount(mount) {
254
+ const config = await this.readConfigFile();
255
+ const mounts = config.mounts ?? [];
256
+ const entry = {
257
+ path: mount.path,
258
+ uri: mount.uri
259
+ };
260
+ if (mount.description) entry.description = mount.description;
261
+ if (mount.access_mode) entry.access_mode = mount.access_mode;
262
+ if (mount.auth) entry.auth = mount.auth;
263
+ if (mount.namespace) entry.namespace = mount.namespace;
264
+ if (mount.options) entry.options = mount.options;
265
+ const existing = mounts.findIndex((m) => m.path === mount.path);
266
+ if (existing >= 0) mounts[existing] = entry;
267
+ else mounts.push(entry);
268
+ config.mounts = mounts;
269
+ await this.writeConfigFile(config);
270
+ }
271
+ async persistRemoveMount(path) {
272
+ const config = await this.readConfigFile();
273
+ config.mounts = (config.mounts ?? []).filter((m) => m.path !== path);
274
+ await this.writeConfigFile(config);
275
+ }
276
+ };
277
+
278
+ //#endregion
279
+ exports.DaemonConfigManager = DaemonConfigManager;