@undefineds.co/xpod 0.4.28 → 0.4.30

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 (179) hide show
  1. package/dist/api/ApiServer.js +4 -0
  2. package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
  3. package/dist/api/ai-gateway/AiGatewayService.js +145 -6
  4. package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
  5. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
  6. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
  7. package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
  8. package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
  9. package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
  10. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
  11. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
  12. package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
  13. package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
  14. package/dist/api/ai-gateway/connect/index.d.ts +40 -0
  15. package/dist/api/ai-gateway/connect/index.js +194 -4
  16. package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
  17. package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
  18. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
  19. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
  20. package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
  21. package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
  22. package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
  23. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
  24. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
  25. package/dist/api/auth/AuthContext.d.ts +4 -0
  26. package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
  27. package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
  28. package/dist/api/auth/SolidSessionFactory.js +114 -4
  29. package/dist/api/chatkit/pod-store.d.ts +8 -1
  30. package/dist/api/chatkit/pod-store.js +54 -3
  31. package/dist/api/container/common.js +34 -60
  32. package/dist/api/container/index.js +0 -3
  33. package/dist/api/container/routes.js +1 -8
  34. package/dist/api/container/types.d.ts +2 -9
  35. package/dist/api/container/types.js +0 -5
  36. package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
  37. package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
  38. package/dist/api/middleware/AuthMiddleware.js +29 -1
  39. package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
  40. package/dist/cli/index.js +2 -1
  41. package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
  42. package/dist/runtime/ingress-port.d.ts +9 -2
  43. package/dist/runtime/ingress-port.js +7 -2
  44. package/dist/runtime/local-secret-file.d.ts +1 -1
  45. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
  46. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
  47. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
  48. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
  49. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
  50. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
  51. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
  52. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
  53. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
  54. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
  55. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
  56. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
  57. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
  58. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
  59. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
  60. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
  61. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
  62. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
  63. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
  64. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
  65. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
  66. package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
  67. package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
  68. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
  69. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
  70. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
  71. package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
  72. package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
  73. package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
  74. package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
  75. package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
  76. package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
  77. package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
  78. package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
  79. package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
  80. package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
  81. package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
  82. package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
  83. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
  84. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
  85. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
  86. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
  87. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
  88. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
  89. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
  90. package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
  91. package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
  92. package/package.json +2 -2
  93. package/static/app/assets/main.css +1 -1
  94. package/static/app/assets/main.js +18 -18
  95. package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
  96. package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
  97. package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
  98. package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
  99. package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
  100. package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
  101. package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
  102. package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
  103. package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
  104. package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
  105. package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
  106. package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
  107. package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
  108. package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
  109. package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
  110. package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
  111. package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
  112. package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
  113. package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
  114. package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
  115. package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
  116. package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
  117. package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
  118. package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
  119. package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
  120. package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
  121. package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
  122. package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
  123. package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
  124. package/static/dashboard/dashboard.html +2 -2
  125. package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
  126. package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
  127. package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
  128. package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
  129. package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
  130. package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
  131. package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
  132. package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
  133. package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
  134. package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
  135. package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
  136. package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
  137. package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
  138. package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
  139. package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
  140. package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
  141. package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
  142. package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
  143. package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
  144. package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
  145. package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
  146. package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
  147. package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
  148. package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
  149. package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
  150. package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
  151. package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
  152. package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
  153. package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
  154. package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
  155. package/static/settings/auth-callback.html +3 -3
  156. package/static/settings/settings.html +3 -3
  157. package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
  158. package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
  159. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
  160. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
  161. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
  162. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
  163. package/dist/runtime/gateway-locator-secret.d.ts +0 -5
  164. package/dist/runtime/gateway-locator-secret.js +0 -30
  165. package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
  166. package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
  167. package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
  168. package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
  169. package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
  170. package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
  171. package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
  172. package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
  173. package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
  174. package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
  175. package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
  176. package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
  177. package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
  178. package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
  179. package/static/settings/assets/pod-collections-host-DjK2xf-b.js +0 -8
@@ -3,7 +3,7 @@ import { type AiOfferingActionError, type AiOfferingQuotaState } from './AiCrede
3
3
  export type { AiProviderDefinition } from './controller.js';
4
4
  import type { AiProviderDefinition } from './controller.js';
5
5
  export type ProviderConnectionState = 'unknown' | 'pending' | 'configured' | 'connected' | 'disconnected' | 'reauthRequired' | 'failed';
6
- export declare function AiProviderCard({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl, busy, disabled, error, quotas, models, verifyPending, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onVerify, onAddModel, onEditModel, onDeleteModel, selectedModelIds, modelSelectionStatus, onModelSelectionChange, onDismissError, }: {
6
+ export declare function AiProviderCard({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl, busy, disabled, developerMode, error, quotas, models, verifyPending, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onVerify, onAddModel, onEditModel, onDeleteModel, selectedModelIds, modelSelectionStatus, onModelSelectionChange, onDismissError, }: {
7
7
  definition: AiProviderDefinition;
8
8
  product?: AiProviderSummary;
9
9
  status: ProviderConnectionState;
@@ -14,6 +14,7 @@ export declare function AiProviderCard({ definition, product, status, accountLab
14
14
  baseUrl?: string;
15
15
  busy: boolean;
16
16
  disabled?: boolean;
17
+ developerMode?: boolean;
17
18
  error?: AiOfferingActionError;
18
19
  quotas?: Partial<Record<string, AiOfferingQuotaState>>;
19
20
  models: AiGatewayModel[];
@@ -8,7 +8,7 @@ import { AiProviderHeader } from './AiProviderHeader.js';
8
8
  import { authorizationMethodsForOffering, isBrowserConnectMethod } from './authorization-methods.js';
9
9
  import { AiCredentialPoolSection, } from './AiCredentialPoolSection.js';
10
10
  import { AiModelSearchInput, AiModelRow, modelIconTokens, } from './AiModelCatalog.js';
11
- export function AiProviderCard({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl = '', busy, disabled = false, error, quotas, models, verifyPending = false, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onVerify, onAddModel, onEditModel, onDeleteModel, selectedModelIds, modelSelectionStatus, onModelSelectionChange, onDismissError, }) {
11
+ export function AiProviderCard({ definition, product, status, accountLabel, attempt, attemptOfferingId, apiKey, baseUrl = '', busy, disabled = false, developerMode = false, error, quotas, models, verifyPending = false, onApiKeyChange, onBaseUrlChange, onBeginApiKey, onBeginOffering, onCancelConnect, onBeginBrowser, onSaveApiKey, onDisconnect, onCreateApiKeyCredential, onCreateLocalCredential, onUpdateCredential, onDeleteCredential, onTestCredential, onReorderCredentials, onRefreshQuota, onVerify, onAddModel, onEditModel, onDeleteModel, selectedModelIds, modelSelectionStatus, onModelSelectionChange, onDismissError, }) {
12
12
  const workbench = product?.offerings.find((offering) => offering.consoleUrl
13
13
  && authorizationMethodsForOffering(offering).some(isBrowserConnectMethod))?.consoleUrl;
14
14
  const isConfigured = status === 'configured';
@@ -44,7 +44,7 @@ export function AiProviderCard({ definition, product, status, accountLabel, atte
44
44
  setLocalSelectedModelIds(nextModelIds);
45
45
  onModelSelectionChange?.(definition.id, nextModelIds);
46
46
  };
47
- return (_jsx(TooltipProvider, { children: _jsxs("div", { className: "space-y-8", children: [_jsx(AiProviderHeader, { name: definition.name, mark: providerMark(definition.id), avatar: getProviderAvatar(definition.id), avatarBackground: getProviderAvatarBackground(definition.id), infoLabel: "\u63D0\u4F9B\u5546\u8BF4\u660E", infoLines: [definition.description, '连接凭据保存在当前 Pod,由 Pod 权限保护。'], link: { href: definition.homeUrl, label: '访问官网' }, links: workbench ? [{ href: workbench, label: '打开工作台' }] : [], badge: (_jsx(Badge, { variant: isConnected || isConfigured ? 'default' : 'secondary', children: connectionStatusLabel(status) })) }), _jsx(AiCredentialPoolSection, { definition: definition, product: product, status: status, accountLabel: accountLabel, attempt: attempt, attemptOfferingId: attemptOfferingId, apiKey: apiKey, baseUrl: baseUrl, busy: busy, disabled: disabled, error: error, suppressError: Boolean(catalogError), quotas: quotas, onApiKeyChange: onApiKeyChange, onBaseUrlChange: onBaseUrlChange, onBeginApiKey: onBeginApiKey, onBeginOffering: onBeginOffering, onCancelConnect: onCancelConnect, onBeginBrowser: onBeginBrowser, onSaveApiKey: onSaveApiKey, onDisconnect: onDisconnect, onCreateApiKeyCredential: product?.offerings.length ? onCreateApiKeyCredential : undefined, onCreateLocalCredential: product?.offerings.length ? onCreateLocalCredential : undefined, onUpdateCredential: onUpdateCredential, onDeleteCredential: onDeleteCredential, onTestCredential: onTestCredential, onReorderCredentials: onReorderCredentials, onRefreshQuota: onRefreshQuota, onDismissError: onDismissError }), _jsxs("section", { className: "space-y-8", children: [_jsxs("div", { "data-testid": "provider-models-header", className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium text-foreground/90", children: [_jsx(Box, { "aria-hidden": "true", className: "h-4 w-4 text-primary" }), "\u53EF\u7528\u6A21\u578B"] }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u5171 ", catalog.length, " \u00B7 \u5DF2\u52A0\u5165 ", selectedModelCount, " \u00B7 \u5DF2\u5931\u6548 ", unavailableModelCount] }), modelSelectionStatus === 'saving' ? (_jsxs("span", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Loader2, { "aria-hidden": "true", className: "h-3 w-3 animate-spin motion-reduce:animate-none" }), "\u4FDD\u5B58\u4E2D\u2026"] })) : null, modelSelectionStatus === 'saved' ? (_jsxs("span", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Check, { "aria-hidden": "true", className: "h-3 w-3" }), "\u5DF2\u4FDD\u5B58"] })) : null] }), _jsxs("div", { "data-testid": "provider-models-actions", className: "flex w-full flex-wrap items-center gap-2 sm:w-auto sm:flex-nowrap sm:justify-end", children: [(isConfigured || isConnected) && (onVerify || onAddModel) ? (_jsxs(_Fragment, { children: [onAddModel ? (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: disabled || busy, onClick: onAddModel, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "\u6DFB\u52A0\u6A21\u578B"] })) : null, onVerify && models.length > 0 ? (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: disabled || busy || verifyPending, onClick: onVerify, children: [verifyPending
47
+ return (_jsx(TooltipProvider, { children: _jsxs("div", { className: "space-y-8", children: [_jsx(AiProviderHeader, { name: definition.name, mark: providerMark(definition.id), avatar: getProviderAvatar(definition.id), avatarBackground: getProviderAvatarBackground(definition.id), infoLabel: "\u63D0\u4F9B\u5546\u8BF4\u660E", infoLines: [definition.description, '连接凭据保存在当前 Pod,由 Pod 权限保护。'], link: { href: definition.homeUrl, label: '访问官网' }, links: workbench ? [{ href: workbench, label: '打开工作台' }] : [], badge: (_jsx(Badge, { variant: isConnected || isConfigured ? 'default' : 'secondary', children: connectionStatusLabel(status) })) }), _jsx(AiCredentialPoolSection, { definition: definition, product: product, status: status, accountLabel: accountLabel, attempt: attempt, attemptOfferingId: attemptOfferingId, apiKey: apiKey, baseUrl: baseUrl, busy: busy, disabled: disabled, developerMode: developerMode, error: error, suppressError: Boolean(catalogError), quotas: quotas, onApiKeyChange: onApiKeyChange, onBaseUrlChange: onBaseUrlChange, onBeginApiKey: onBeginApiKey, onBeginOffering: onBeginOffering, onCancelConnect: onCancelConnect, onBeginBrowser: onBeginBrowser, onSaveApiKey: onSaveApiKey, onDisconnect: onDisconnect, onCreateApiKeyCredential: product?.offerings.length ? onCreateApiKeyCredential : undefined, onCreateLocalCredential: product?.offerings.length ? onCreateLocalCredential : undefined, onUpdateCredential: onUpdateCredential, onDeleteCredential: onDeleteCredential, onTestCredential: onTestCredential, onReorderCredentials: onReorderCredentials, onRefreshQuota: onRefreshQuota, onDismissError: onDismissError }), _jsxs("section", { className: "space-y-8", children: [_jsxs("div", { "data-testid": "provider-models-header", className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsxs("h3", { className: "flex items-center gap-2 text-sm font-medium text-foreground/90", children: [_jsx(Box, { "aria-hidden": "true", className: "h-4 w-4 text-primary" }), "\u53EF\u7528\u6A21\u578B"] }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u5171 ", catalog.length, " \u00B7 \u5DF2\u52A0\u5165 ", selectedModelCount, " \u00B7 \u5DF2\u5931\u6548 ", unavailableModelCount] }), modelSelectionStatus === 'saving' ? (_jsxs("span", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Loader2, { "aria-hidden": "true", className: "h-3 w-3 animate-spin motion-reduce:animate-none" }), "\u4FDD\u5B58\u4E2D\u2026"] })) : null, modelSelectionStatus === 'saved' ? (_jsxs("span", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Check, { "aria-hidden": "true", className: "h-3 w-3" }), "\u5DF2\u4FDD\u5B58"] })) : null] }), _jsxs("div", { "data-testid": "provider-models-actions", className: "flex w-full flex-wrap items-center gap-2 sm:w-auto sm:flex-nowrap sm:justify-end", children: [(isConfigured || isConnected) && (onVerify || onAddModel) ? (_jsxs(_Fragment, { children: [onAddModel ? (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: disabled || busy, onClick: onAddModel, children: [_jsx(Plus, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), "\u6DFB\u52A0\u6A21\u578B"] })) : null, onVerify && models.length > 0 ? (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: disabled || busy || verifyPending, onClick: onVerify, children: [verifyPending
48
48
  ? _jsx(Loader2, { "aria-hidden": "true", className: "h-3.5 w-3.5 animate-spin" })
49
49
  : _jsx(RotateCw, { "aria-hidden": "true", className: "h-3.5 w-3.5" }), verifyPending ? '同步中...' : '刷新模型'] })) : onVerify ? (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8 gap-1.5 text-xs", disabled: disabled || busy || verifyPending, onClick: onVerify, children: [verifyPending
50
50
  ? _jsx(Loader2, { "aria-hidden": "true", className: "h-3.5 w-3.5 animate-spin" })
@@ -8,5 +8,5 @@ import { AiInfoTooltip } from './AiInfoTooltip.js';
8
8
  * anatomy lives here once instead of being re-typed per page.
9
9
  */
10
10
  export function AiProviderHeader({ name, mark, avatar, avatarBackground, infoLabel, infoLines, link, links = [], badge, }) {
11
- return (_jsxs("header", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Avatar, { className: "h-9 w-9 shrink-0 rounded-lg border border-border/50 bg-muted/50 shadow-sm", style: avatarBackground ? { backgroundColor: avatarBackground } : undefined, children: [_jsx(AvatarImage, { src: avatar, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-lg bg-transparent text-sm font-bold uppercase text-muted-foreground", children: mark })] }), _jsxs("div", { className: "flex flex-col justify-center gap-0.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h2", { className: "text-base font-semibold leading-none tracking-tight text-foreground", children: name }), _jsx(AiInfoTooltip, { label: infoLabel, lines: infoLines })] }), _jsx("div", { role: "group", "aria-label": `${name}官方链接`, className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [link, ...links].map((item) => (_jsxs("a", { href: item.href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-0.5 text-[10px] leading-none text-muted-foreground transition-colors hover:text-primary", children: [item.label, " ", _jsx(ExternalLink, { "aria-hidden": "true", className: "h-2.5 w-2.5" })] }, item.href))) })] })] }), badge] }));
11
+ return (_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsxs(Avatar, { className: "h-9 w-9 shrink-0 rounded-lg border border-border/50 bg-muted/50 shadow-sm", style: avatarBackground ? { backgroundColor: avatarBackground } : undefined, children: [_jsx(AvatarImage, { src: avatar, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-lg bg-transparent text-sm font-bold uppercase text-muted-foreground", children: mark })] }), _jsxs("div", { className: "flex min-w-0 flex-col justify-center gap-0.5", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("h2", { className: "text-base font-semibold leading-none tracking-tight text-foreground", children: name }), _jsx(AiInfoTooltip, { label: infoLabel, lines: infoLines })] }), _jsx("div", { role: "group", "aria-label": `${name}官方链接`, className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [link, ...links].map((item) => (_jsxs("a", { href: item.href, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-0.5 text-[10px] leading-none text-muted-foreground transition-colors hover:text-primary", children: [item.label, " ", _jsx(ExternalLink, { "aria-hidden": "true", className: "h-2.5 w-2.5" })] }, item.href))) })] })] }), badge ? _jsx("div", { className: "shrink-0 whitespace-nowrap", children: badge }) : null] }));
12
12
  }
@@ -0,0 +1 @@
1
+ export declare function apiKeyFingerprint(apiKey: string): Promise<string | undefined>;
@@ -0,0 +1,7 @@
1
+ export async function apiKeyFingerprint(apiKey) {
2
+ const subtle = globalThis.crypto?.subtle;
3
+ if (!subtle)
4
+ return undefined;
5
+ const digest = await subtle.digest('SHA-256', new TextEncoder().encode(apiKey));
6
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
7
+ }
@@ -1,4 +1,4 @@
1
- import type { AiConnectAttempt, AiConnectionsCredential, AiConnectionsProvider, AiGatewayModel, AiGatewayCatalogModel, AiProviderAuthorizationMethodsSummary, AiProviderCredentialSummary, AiProviderSummary, CustomProviderModel, GatewayKeyRecord, ProviderModelDiscovery } from './types.js';
1
+ import type { AiConnectAttempt, AiConnectionsCredential, AiConnectionsProvider, AiGatewayModel, AiGatewayCatalogModel, AiProviderAuthorizationMethodsSummary, AiProviderCredentialSummary, AiProviderSummary, CustomProviderModel, ProviderModelDiscovery } from './types.js';
2
2
  /**
3
3
  * Everything that turns untrusted API payloads, user input and failures into the
4
4
  * shapes the rest of the app can rely on: proxy/pod URL normalising, error text,
@@ -38,7 +38,6 @@ export declare class AiConnectionsRequestError extends Error {
38
38
  export declare function aiConnectionsErrorCode(payload: unknown): string | undefined;
39
39
  export declare function readJson(response: Response): Promise<unknown>;
40
40
  export declare function assertProvider(provider: string): asserts provider is AiConnectionsProvider;
41
- export declare function parseGatewayKeyRecord(value: unknown): GatewayKeyRecord | undefined;
42
41
  export declare function parseCustomModelList(value: unknown): CustomProviderModel[];
43
42
  export declare function parseGatewayModel(value: unknown): AiGatewayModel | undefined;
44
43
  export declare function parseGatewayCatalogModel(value: unknown): AiGatewayCatalogModel | undefined;
@@ -101,39 +101,6 @@ export function assertProvider(provider) {
101
101
  throw new Error(`Unsupported AI provider: ${provider}`);
102
102
  }
103
103
  }
104
- export function parseGatewayKeyRecord(value) {
105
- if (!isRecord(value)
106
- || typeof value.id !== 'string'
107
- || typeof value.owner !== 'string'
108
- || !Array.isArray(value.scopes)
109
- || !value.scopes.every((scope) => typeof scope === 'string')
110
- || typeof value.createdAt !== 'string') {
111
- return undefined;
112
- }
113
- return compactObject({
114
- id: value.id,
115
- kind: value.kind === 'client-credentials' ? value.kind : undefined,
116
- credentialResource: stringValue(value.credentialResource),
117
- fingerprint: stringValue(value.fingerprint),
118
- owner: value.owner,
119
- scopes: value.scopes,
120
- createdAt: value.createdAt,
121
- expiresAt: stringValue(value.expiresAt),
122
- lastUsedAt: stringValue(value.lastUsedAt),
123
- disabledAt: stringValue(value.disabledAt),
124
- revokedAt: stringValue(value.revokedAt),
125
- name: stringValue(value.name),
126
- maskedHint: stringValue(value.maskedHint),
127
- plaintextAvailable: typeof value.plaintextAvailable === 'boolean' ? value.plaintextAvailable : undefined,
128
- // Recorded by the server when the credential was issued/applied; the app
129
- // needs them to revoke the CSS credential without a stored secret.
130
- clientCredentialId: stringValue(value.clientCredentialId),
131
- appliedTo: stringValue(value.appliedTo),
132
- appliedOn: stringValue(value.appliedOn),
133
- appliedAt: stringValue(value.appliedAt),
134
- appliedClients: stringListValue(value.appliedClients),
135
- });
136
- }
137
104
  export function parseCustomModelList(value) {
138
105
  if (!Array.isArray(value)) {
139
106
  throw new Error('AI Connection returned an invalid custom models response');
@@ -4,4 +4,4 @@ import type { AiConnectionsClient, CreateAiConnectionsClientInput } from './type
4
4
  * fetch, with every response normalised by `normalize.ts`.
5
5
  */
6
6
  export declare function resolveAiConnectionsApiBase(podBaseUrl: string): string;
7
- export declare function createAiConnectionsClient({ webId, podBaseUrl, authenticatedFetch, }: CreateAiConnectionsClientInput): AiConnectionsClient;
7
+ export declare function createAiConnectionsClient({ webId, podBaseUrl, authenticatedFetch, clientCredentials, }: CreateAiConnectionsClientInput): AiConnectionsClient;
@@ -1,4 +1,5 @@
1
- import { AiConnectionsRequestError, assertProvider, compactObject, isDefined, isRecord, parseAuthorizationMethodsSummary, parseConnectAttempt, parseCredential, parseCustomModelList, parseGatewayKeyRecord, parseGatewayModel, parseGatewayCatalogModel, parseModelDiscovery, parseProviderCredentialSummary, parseProviderSummaries, readJson, sanitizePublicObject, } from './normalize.js';
1
+ import { apiKeyFingerprint } from './fingerprint.js';
2
+ import { AiConnectionsRequestError, assertProvider, compactObject, isDefined, isRecord, parseAuthorizationMethodsSummary, parseConnectAttempt, parseCredential, parseCustomModelList, parseGatewayModel, parseGatewayCatalogModel, parseModelDiscovery, parseProviderCredentialSummary, parseProviderSummaries, readJson, sanitizePublicObject, } from './normalize.js';
2
3
  /**
3
4
  * The HTTP client itself: one factory over the Pod base URL and an authenticated
4
5
  * fetch, with every response normalised by `normalize.ts`.
@@ -10,8 +11,44 @@ export function resolveAiConnectionsApiBase(podBaseUrl) {
10
11
  }
11
12
  return parsed.origin;
12
13
  }
13
- export function createAiConnectionsClient({ webId, podBaseUrl, authenticatedFetch, }) {
14
+ export function createAiConnectionsClient({ webId, podBaseUrl, authenticatedFetch, clientCredentials, }) {
14
15
  const apiBase = resolveAiConnectionsApiBase(podBaseUrl);
16
+ /**
17
+ * Xpod key management is Account client-credentials management. A client that
18
+ * was built without the capability fails closed: it must never fall back to
19
+ * the Gateway key routes that this product no longer has, and the contract
20
+ * layer carries the fact (`account_client_credentials_unavailable`) rather
21
+ * than the sentence the applet shows.
22
+ */
23
+ const accountCredentials = () => {
24
+ if (!clientCredentials) {
25
+ throw new AiConnectionsRequestError({ code: 'account_client_credentials_unavailable' }, 0);
26
+ }
27
+ return clientCredentials;
28
+ };
29
+ /**
30
+ * The wrapper only exists in the session that issued it, so its digest is a
31
+ * session-local observation: refreshing the Account list must not erase the
32
+ * ability to verify the exact key just applied, and a reload must not invent
33
+ * one. Nothing here is persisted; the Account remains the only key index.
34
+ */
35
+ const observedFingerprints = new Map();
36
+ const accountEntryRecord = (entry, options = {}) => ({
37
+ id: entry.clientId,
38
+ kind: 'client-credentials',
39
+ credentialResource: entry.resource,
40
+ clientCredentialId: entry.clientId,
41
+ // The digest of the applied wrapper (identical to what the native adapter
42
+ // reports), never the Account client id: the UI compares the two, so a
43
+ // browser-side id here would make every verify look like a key change.
44
+ ...(options.fingerprint ? { fingerprint: options.fingerprint } : {}),
45
+ // Only the Account-verified WebID counts as ownership. An entry whose
46
+ // owner is unknown is never relabelled as the selected identity.
47
+ owner: entry.webId ?? webId,
48
+ name: options.name ?? entry.label ?? entry.clientId,
49
+ // The wrapper is only ever visible in the session that created it.
50
+ plaintextAvailable: false,
51
+ });
15
52
  const request = async (path, method, body, context = {}) => {
16
53
  const response = await authenticatedFetch(`${apiBase}${path}`, {
17
54
  method,
@@ -19,6 +56,7 @@ export function createAiConnectionsClient({ webId, podBaseUrl, authenticatedFetc
19
56
  mode: 'cors',
20
57
  headers: {
21
58
  accept: 'application/json',
59
+ 'X-Xpod-Pod-Url': podBaseUrl,
22
60
  ...(body ? { 'content-type': 'application/json' } : {}),
23
61
  },
24
62
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -81,30 +119,38 @@ export function createAiConnectionsClient({ webId, podBaseUrl, authenticatedFetc
81
119
  return payload.data.map(parseGatewayCatalogModel).filter(isDefined);
82
120
  },
83
121
  async listGatewayKeys() {
84
- const payload = await request('/api/ai/gateway/keys', 'GET');
85
- return Array.isArray(payload.data)
86
- ? payload.data.map(parseGatewayKeyRecord).filter(isDefined)
87
- : [];
122
+ const entries = await accountCredentials().list();
123
+ return entries
124
+ // The Account can own credentials for several WebIDs; this page only
125
+ // ever shows the selected authenticated identity's rows.
126
+ .filter((entry) => entry.webId === webId)
127
+ .map((entry) => accountEntryRecord(entry, { fingerprint: observedFingerprints.get(entry.clientId) }));
88
128
  },
89
129
  async createGatewayKey(input) {
90
- const payload = await request('/api/ai/gateway/keys', 'POST', compactObject(input));
91
- if (typeof payload.key !== 'string' || !payload.key) {
92
- throw new Error('Xpod did not return the new API Key');
93
- }
94
- const record = parseGatewayKeyRecord(payload.record);
95
- if (!record)
96
- throw new Error('Xpod returned an invalid API Key record');
97
- return { plaintext: payload.key, record };
98
- },
99
- async updateGatewayKey(keyId, input) {
100
- const payload = await request(`/api/ai/gateway/keys/${encodeURIComponent(keyId)}`, 'PATCH', input);
101
- const record = parseGatewayKeyRecord(payload.record);
102
- if (!record)
103
- throw new Error('Xpod returned an invalid API Key record');
104
- return record;
130
+ const issued = await accountCredentials().create({ name: input.name, webId });
131
+ const fingerprint = await apiKeyFingerprint(issued.apiKey);
132
+ if (fingerprint)
133
+ observedFingerprints.set(issued.clientId, fingerprint);
134
+ return {
135
+ plaintext: issued.apiKey,
136
+ record: {
137
+ ...accountEntryRecord({ clientId: issued.clientId, label: input.name, resource: issued.resource, webId }, { name: input.name, fingerprint }),
138
+ // The wrapper exists right now, in this session: this row is not the
139
+ // "cannot show it again" case the plain list row is.
140
+ plaintextAvailable: undefined,
141
+ ...(input.appliedTo ? { appliedTo: input.appliedTo } : {}),
142
+ },
143
+ };
105
144
  },
106
145
  async deleteGatewayKey(keyId) {
107
- await request(`/api/ai/gateway/keys/${encodeURIComponent(keyId)}`, 'DELETE');
146
+ const credentials = accountCredentials();
147
+ const target = (await credentials.list())
148
+ .find((entry) => entry.clientId === keyId && entry.webId === webId);
149
+ // The Account is the only key index, so a credential it no longer lists is
150
+ // a fact the caller reports, not a Pod record this client can recreate.
151
+ if (!target)
152
+ throw new AiConnectionsRequestError({ code: 'account_client_credential_not_found' }, 0);
153
+ await credentials.revoke({ clientId: target.clientId, resource: target.resource, webId });
108
154
  },
109
155
  async beginConnect(provider, mode, options) {
110
156
  return await requestConnect(provider, '/connect/begin', 'POST', compactObject({
@@ -2,7 +2,7 @@ import type { AIModelClass } from '@undefineds.co/models';
2
2
  /**
3
3
  * Wire and view types of the AI connections API. No behaviour.
4
4
  */
5
- import type { AiConnectionsModelSelection, AiConnectionsOAuthCredential } from '@undefineds.co/extension-sdk/web';
5
+ import type { AiClientCredentialsCapability, AiConnectionsModelSelection, AiConnectionsOAuthCredential } from '@undefineds.co/extension-sdk/web';
6
6
  export declare const AI_CONNECTIONS_PROVIDERS: readonly ["openai", "anthropic", "kimi", "bailian", "deepseek", "zhipu", "ollama", "custom"];
7
7
  export type AiConnectionsProvider = (typeof AI_CONNECTIONS_PROVIDERS)[number];
8
8
  export type AiConnectionsMode = 'browserAssistedApiKey' | 'deviceCodeOAuth' | 'authorizationCodeOAuth' | 'connectUnsupported';
@@ -57,14 +57,32 @@ export interface AiQuotaSnapshot {
57
57
  source: string;
58
58
  stale?: boolean;
59
59
  }
60
+ /**
61
+ * One row of the Xpod key list as the page renders it.
62
+ *
63
+ * The Account owns the credential, so a record carries identity
64
+ * (`id`/`clientCredentialId`/`credentialResource`) and the label the user gave
65
+ * it. `lastUsedAt`/`appliedAt`/`expiresAt`/`appliedTo` are session-local
66
+ * observations or absent: the Account does not own them, and nothing here is a
67
+ * second key index.
68
+ */
60
69
  export interface GatewayKeyRecord {
61
70
  id: string;
62
71
  kind?: 'client-credentials';
63
72
  credentialResource?: string;
73
+ /**
74
+ * Digest of the `sk-` wrapper the known client configuration was written with
75
+ * (the same value the native adapters report as `apiKeyFingerprint`), never a
76
+ * secret. It is a session-local observation: the Account owns no wrapper, so a
77
+ * row restored from the Account list has no digest and cannot be tested. The
78
+ * credential's equality identity stays `clientCredentialId`.
79
+ */
64
80
  fingerprint?: string;
65
81
  owner: string;
66
- scopes: string[];
67
- createdAt: string;
82
+ /** Account-owned scopes are not projected here; the wrapper's reach is the Pod. */
83
+ scopes?: string[];
84
+ /** Account credits carry no creation timestamp; absent rather than invented. */
85
+ createdAt?: string;
68
86
  expiresAt?: string;
69
87
  lastUsedAt?: string;
70
88
  disabledAt?: string;
@@ -283,19 +301,22 @@ export interface AiConnectionsClient {
283
301
  listGatewayModels?(): Promise<AiGatewayModel[]>;
284
302
  /** Complete Gateway directory, including platform routing roles and unattributed entries. */
285
303
  listGatewayCatalogModels?(): Promise<AiGatewayCatalogModel[]>;
304
+ /**
305
+ * Xpod key management is Account client-credentials management. These three
306
+ * calls are backed by the host's `AiClientCredentialsCapability` and never by
307
+ * a Gateway key route: a client without that capability fails closed.
308
+ *
309
+ * There is deliberately no enable/disable call - the Account has no such
310
+ * capability - and no reveal call: the `sk-` wrapper exists only in the create
311
+ * response and in `plaintexts` for the session that issued it.
312
+ */
286
313
  listGatewayKeys(): Promise<GatewayKeyRecord[]>;
287
314
  createGatewayKey(input: {
288
315
  name: string;
289
- apiKey?: string;
290
316
  credentialResource?: string;
291
- /** Client application the credential is written into; recorded with the key. */
317
+ /** Client application the key is meant for; a session-local annotation only. */
292
318
  appliedTo?: string;
293
- scopes?: string[];
294
- expiresAt?: string;
295
319
  }): Promise<CreatedGatewayKey>;
296
- updateGatewayKey(keyId: string, input: {
297
- enabled: boolean;
298
- }): Promise<GatewayKeyRecord>;
299
320
  deleteGatewayKey(keyId: string): Promise<void>;
300
321
  beginConnect(provider: AiConnectionsProvider, mode: AiConnectionsMode, options?: AiConnectionBeginOptions): Promise<AiConnectAttempt>;
301
322
  connectStatus(provider: AiConnectionsProvider, attempt: Pick<AiConnectAttempt, 'attemptId' | 'state' | 'signature' | 'offeringId'> & Partial<Pick<AiConnectAttempt, 'mode'>>): Promise<AiConnectAttempt>;
@@ -349,4 +370,10 @@ export interface CreateAiConnectionsClientInput {
349
370
  webId: string;
350
371
  podBaseUrl: string;
351
372
  authenticatedFetch: typeof fetch;
373
+ /**
374
+ * The Account client-credentials capability that backs Xpod key management.
375
+ * Optional at the type level so a Pod-only client still type-checks, but the
376
+ * three key calls fail closed without it.
377
+ */
378
+ clientCredentials?: AiClientCredentialsCapability;
352
379
  }
@@ -121,11 +121,14 @@ export function createAiConnectionsController(host) {
121
121
  return guard;
122
122
  };
123
123
  const client = authenticated
124
- ? createInteractiveAiConnectionsClient(withAccountClientCredentials(createAiConnectionsClient({
124
+ ? createInteractiveAiConnectionsClient(createAiConnectionsClient({
125
125
  webId: sessionSnapshot.webId,
126
126
  podBaseUrl: readyPod.current.podUrl,
127
127
  authenticatedFetch: host.solid.session.fetch,
128
- }), host.capabilities.aiClientCredentials), host.capabilities.aiConnectionsPodStore, liveCredentials, beginProviderLoad)
128
+ // Account client credentials are the only Xpod key store there is; the
129
+ // key calls are backed by this capability and never by a Pod route.
130
+ clientCredentials: host.capabilities.aiClientCredentials,
131
+ }), host.capabilities.aiConnectionsPodStore, liveCredentials, beginProviderLoad)
129
132
  : null;
130
133
  let selectedSection = 'keys';
131
134
  let selectedProvider = 'openai';
@@ -445,50 +448,6 @@ export function createAiConnectionsController(host) {
445
448
  };
446
449
  return controller;
447
450
  }
448
- function withAccountClientCredentials(client, credentials) {
449
- return {
450
- ...client,
451
- async createGatewayKey(input) {
452
- if (!credentials)
453
- throw new Error('当前账号登录状态不支持创建客户端凭据。');
454
- const issued = await credentials.create({ name: input.name, webId: client.webId });
455
- try {
456
- // The wrapper is issued here; the declared purpose and the rest of the
457
- // caller's input travel with it so the record keeps where it applies.
458
- return await client.createGatewayKey({
459
- ...input,
460
- apiKey: issued.apiKey,
461
- credentialResource: issued.resource,
462
- });
463
- }
464
- catch (cause) {
465
- try {
466
- await credentials.revoke({
467
- clientId: clientIdFromApiKey(issued.apiKey), resource: issued.resource, webId: client.webId,
468
- });
469
- }
470
- catch {
471
- throw new Error('API Key 未能保存到 Pod,且账号凭据撤销失败。该凭据尚未应用到客户端。');
472
- }
473
- throw cause;
474
- }
475
- },
476
- async deleteGatewayKey(keyId) {
477
- const record = (await client.listGatewayKeys()).find((key) => key.id === keyId);
478
- if (record?.kind === 'client-credentials') {
479
- if (!credentials || !record.clientCredentialId) {
480
- throw new Error('当前账号登录状态无法撤销此客户端凭据,Key 记录已保留。');
481
- }
482
- const issued = await credentials.list();
483
- const target = issued.find((entry) => entry.clientId === record.clientCredentialId);
484
- if (!target)
485
- throw new Error('账号服务中已找不到该客户端凭据,请刷新后重试。');
486
- await credentials.revoke({ clientId: target.clientId, resource: target.resource, webId: client.webId });
487
- }
488
- await client.deleteGatewayKey(keyId);
489
- },
490
- };
491
- }
492
451
  function createInteractiveAiConnectionsClient(operationsClient, podStore, live, beginProviderLoad) {
493
452
  const listProviders = async () => {
494
453
  const guard = beginProviderLoad();
@@ -1032,11 +991,3 @@ function isDefined(value) {
1032
991
  function errorMessage(error) {
1033
992
  return aiConnectionsErrorMessage(error);
1034
993
  }
1035
- /** The wrapper carries the OIDC client id; parsing it needs no stored secret. */
1036
- function clientIdFromApiKey(apiKey) {
1037
- const decoded = atob(apiKey.replace(/^sk-/u, ''));
1038
- const separator = decoded.indexOf(':');
1039
- if (separator <= 0)
1040
- throw new Error('客户端凭据格式无效。');
1041
- return decoded.slice(0, separator);
1042
- }
@@ -171,10 +171,15 @@ function messageForSafeErrorCode(code, provider) {
171
171
  return '模型已获取,但保存到 Pod 失败。请重试同步模型。';
172
172
  case 'quota_credential_not_found':
173
173
  return '当前身份没有可用的额度凭证。';
174
+ case 'account_client_credentials_unavailable':
175
+ // Xpod keys are Account client credentials; a host without the Account
176
+ // capability has no key surface at all, so the user must re-enter from a
177
+ // signed-in Account session.
178
+ return 'Xpod 密钥 需要当前 Account 登录状态:这个客户端没有 Account 客户端凭据能力。';
179
+ case 'account_client_credential_not_found':
180
+ return '账号服务中已找不到该客户端凭据,请刷新后重试。';
174
181
  case 'credential_secret_unavailable':
175
182
  return '当前凭证密钥不可用,请重新保存后再查询额度。';
176
- case 'gateway_api_key_plaintext_unavailable':
177
- return 'Pod 中未找到此 API Key 的原文,无法复制配置。请创建新的 Key,更新客户端后再删除旧 Key。';
178
183
  case 'quota_adapter_not_found':
179
184
  return '该接入方式不支持查询官方额度。';
180
185
  default: