@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
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.FileSessionImportAdapter = void 0;
7
7
  const node_fs_1 = require("node:fs");
8
8
  const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
9
10
  const DEFAULT_MAX_SESSION_FILE_BYTES = 256 * 1024;
10
11
  class FileSessionImportAdapter {
11
12
  constructor(options) {
@@ -13,6 +14,7 @@ class FileSessionImportAdapter {
13
14
  this.provider = options.profile.provider;
14
15
  this.offeringId = options.profile.offeringId;
15
16
  this.homeDir = options.homeDir ?? node_os_1.default.homedir();
17
+ this.env = options.env ?? process.env;
16
18
  this.readFile = options.readFile ?? node_fs_1.promises.readFile;
17
19
  this.maxBytes = options.maxBytes ?? DEFAULT_MAX_SESSION_FILE_BYTES;
18
20
  }
@@ -20,9 +22,19 @@ class FileSessionImportAdapter {
20
22
  if (input.deployment !== 'local') {
21
23
  throw new Error('local_session_import_unavailable_in_cloud');
22
24
  }
23
- const raw = await this.readSessionFile(this.profile.resolvePath(this.homeDir));
25
+ const filePath = this.profile.resolvePath(this.homeDir, this.env);
26
+ const raw = await this.readSessionFile(filePath);
24
27
  const payload = parseJsonObject(raw);
25
- return this.profile.importSession(payload);
28
+ const result = this.profile.importSession(payload);
29
+ return {
30
+ ...result,
31
+ // The profile declares the path; the adapter reports the file it actually read so the
32
+ // path can never drift from the metadata the way a duplicated literal did before.
33
+ metadata: withoutUndefined({
34
+ ...result.metadata,
35
+ sessionPath: homeRelativePath(this.homeDir, filePath),
36
+ }),
37
+ };
26
38
  }
27
39
  async readSessionFile(filePath) {
28
40
  let raw;
@@ -30,7 +42,7 @@ class FileSessionImportAdapter {
30
42
  raw = await this.readFile(filePath, 'utf8');
31
43
  }
32
44
  catch (error) {
33
- if (isErrno(error, 'ENOENT')) {
45
+ if (isErrno(error, 'ENOENT') || isErrno(error, 'ENOTDIR')) {
34
46
  throw new Error('local_session_file_missing');
35
47
  }
36
48
  throw new Error('local_session_file_read_failed');
@@ -42,6 +54,11 @@ class FileSessionImportAdapter {
42
54
  }
43
55
  }
44
56
  exports.FileSessionImportAdapter = FileSessionImportAdapter;
57
+ /** `~/…` form for files under the home directory; undefined for any other location. */
58
+ function homeRelativePath(homeDir, filePath) {
59
+ const prefix = homeDir.endsWith(node_path_1.default.sep) ? homeDir : `${homeDir}${node_path_1.default.sep}`;
60
+ return filePath.startsWith(prefix) ? `~${node_path_1.default.sep}${filePath.slice(prefix.length)}` : undefined;
61
+ }
45
62
  function parseJsonObject(raw) {
46
63
  let parsed;
47
64
  try {
@@ -60,4 +77,7 @@ function isErrno(error, code) {
60
77
  && 'code' in error
61
78
  && error.code === code;
62
79
  }
80
+ function withoutUndefined(input) {
81
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
82
+ }
63
83
  //# sourceMappingURL=FileSessionImportAdapter.js.map
@@ -1,9 +1,17 @@
1
1
  import type { LocalSessionImportResult } from './FileSessionImportAdapter';
2
+ /**
3
+ * Declarative description of one local session file location.
4
+ *
5
+ * The profile owns the path because each CLI has its own data-directory contract. The adapter
6
+ * stays provider-agnostic: it resolves exactly one path and reads it. `env` exists so a profile can
7
+ * honour an override the installed CLI itself defines; Xpod never adds an environment option of its
8
+ * own, and an explicitly configured override is authoritative (no stale-account fallback).
9
+ */
2
10
  export interface SessionImportProfile {
3
11
  id: string;
4
12
  provider: string;
5
13
  offeringId: string;
6
- resolvePath(homeDir: string): string;
14
+ resolvePath(homeDir: string, env: NodeJS.ProcessEnv): string;
7
15
  importSession(payload: Record<string, unknown>): LocalSessionImportResult;
8
16
  }
9
17
  export declare const OPENAI_CODEX_SESSION_IMPORT_PROFILE: SessionImportProfile;
@@ -16,7 +16,10 @@ exports.KIMI_CODE_SESSION_IMPORT_PROFILE = {
16
16
  id: 'kimi-code-credentials-json',
17
17
  provider: 'kimi',
18
18
  offeringId: 'subscription-key',
19
- resolvePath: (homeDir) => node_path_1.default.join(homeDir, '.kimi', 'credentials', 'kimi-code.json'),
19
+ // The installed Kimi CLI resolves its data directory as KIMI_CODE_HOME ?? ~/.kimi-code and keeps
20
+ // credentials at <dataDir>/credentials/<name>.json. A configured KIMI_CODE_HOME is authoritative:
21
+ // it must not fall back to the default directory or to another subscriber's account.
22
+ resolvePath: (homeDir, env) => node_path_1.default.join(stringValue(env?.KIMI_CODE_HOME) ?? node_path_1.default.join(homeDir, '.kimi-code'), 'credentials', 'kimi-code.json'),
20
23
  importSession: importKimiCodeSession,
21
24
  };
22
25
  function importOpenAiCodexSession(payload) {
@@ -42,7 +45,6 @@ function importOpenAiCodexSession(payload) {
42
45
  accountLabel: accountId ? `OpenAI Subscription ${accountId}` : 'OpenAI Subscription',
43
46
  metadata: withoutUndefined({
44
47
  source: 'local-codex-auth-json',
45
- sessionPath: '~/.codex/auth.json',
46
48
  authMode,
47
49
  accountId,
48
50
  }),
@@ -72,7 +74,6 @@ function importKimiCodeSession(payload) {
72
74
  accountLabel: 'Kimi Subscription',
73
75
  metadata: {
74
76
  source: 'local-kimi-code-credentials-json',
75
- sessionPath: '~/.kimi/credentials/kimi-code.json',
76
77
  },
77
78
  };
78
79
  }
@@ -195,6 +195,11 @@ export interface PodCredentialRepository {
195
195
  provider: string;
196
196
  deployment: GatewayDeployment;
197
197
  reason: string;
198
+ /**
199
+ * Selects the exact credential row. Callers that know which credential failed must pass it so
200
+ * an invalid_grant on one credential can never mark a sibling credential of the same provider.
201
+ */
202
+ credentialId?: string;
198
203
  expectedVersion?: number;
199
204
  auth?: AuthContext;
200
205
  }): Promise<ConnectCredentialRecord | undefined>;
@@ -289,6 +294,7 @@ export declare class PodConnectedCredentialRepository implements PodCredentialRe
289
294
  cooldownUntil?: Date;
290
295
  encryptedSecret: EncryptedCredentialSecret;
291
296
  version?: number;
297
+ expiresAt?: Date;
292
298
  runtimeCredential?: Record<string, unknown>;
293
299
  metadata?: Record<string, unknown>;
294
300
  }>>;
@@ -330,6 +336,7 @@ export declare class PodConnectedCredentialRepository implements PodCredentialRe
330
336
  provider: string;
331
337
  deployment: GatewayDeployment;
332
338
  reason: string;
339
+ credentialId?: string;
333
340
  expectedVersion?: number;
334
341
  auth?: AuthContext;
335
342
  }): Promise<ConnectCredentialRecord | undefined>;
@@ -683,6 +690,39 @@ export declare class ProviderConnectService {
683
690
  }): Promise<AiProviderCredentialSummary>;
684
691
  private importLocalCredential;
685
692
  private refreshImportedSecret;
693
+ /**
694
+ * Exchanges the stored refresh token for a live session. Shared by import-time and use-time
695
+ * renewal; typed reauth failures stay distinguishable so callers can mark the credential.
696
+ */
697
+ private refreshCallerOwnedSecret;
698
+ /**
699
+ * Shared credential lifecycle recovery, called by the inference path when a stored OAuth session
700
+ * is expired or was just rejected upstream.
701
+ *
702
+ * Renewal re-reads the credential row inside a per-credential lock (concurrent callers
703
+ * coalesce), prefers the declared local session source when the live file still belongs to the
704
+ * same subscriber, falls back to the stored refresh token, and persists to the SAME row through
705
+ * the existing CAS update so user settings survive. API keys and local providers keep the
706
+ * existing path: they report "not renewable" instead of being special-cased by provider id.
707
+ *
708
+ * Returns whether a retry with the now-stored credential is worthwhile.
709
+ */
710
+ renewCredential(input: {
711
+ webId: string;
712
+ deployment: GatewayDeployment;
713
+ provider: string;
714
+ credentialId: string;
715
+ observedVersion?: number;
716
+ reason: 'expired' | 'authentication_failed';
717
+ auth?: AuthContext;
718
+ }): Promise<boolean>;
719
+ /**
720
+ * On-demand re-read of the declared local session source for an imported credential: the CLI
721
+ * that owns the file may have rotated it since import. The live session is adopted only when it
722
+ * is unexpired, still belongs to the same subscriber, and does not narrow the granted scopes the
723
+ * stored credential relies on.
724
+ */
725
+ private adoptLiveImportedSession;
686
726
  updateCredential(input: ProviderCredentialQuery & {
687
727
  credentialId: string;
688
728
  expectedVersion: number;
@@ -23,6 +23,12 @@ var LoopbackAuthorizationCallbackReceiver_1 = require("./LoopbackAuthorizationCa
23
23
  Object.defineProperty(exports, "LoopbackAuthorizationCallbackReceiver", { enumerable: true, get: function () { return LoopbackAuthorizationCallbackReceiver_1.LoopbackAuthorizationCallbackReceiver; } });
24
24
  const CREDENTIAL_COLLECTION_QUERY_UNSUPPORTED = 'credential_collection_query_unsupported';
25
25
  const credentialUpdateLocks = new Map();
26
+ /**
27
+ * Serializes on-demand session renewals per (webId, credential) so concurrent in-flight requests
28
+ * coalesce into a single provider refresh. Separate from `credentialUpdateLocks` because the
29
+ * renewal body persists through the CAS update path, which takes that lock itself.
30
+ */
31
+ const credentialRenewalLocks = new Map();
26
32
  class PodConnectedCredentialRepository {
27
33
  constructor(options = {}) {
28
34
  this.podAccess = options.podAccess;
@@ -108,7 +114,7 @@ class PodConnectedCredentialRepository {
108
114
  .filter((record) => record.status === 'active')
109
115
  .filter((record) => normalizeProvider(record.provider) !== '')
110
116
  .filter((record) => providerAllowedByConfiguredIds(record.provider, enabledProviderIds));
111
- const podBaseUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(input.webId, this.podBaseUrlResolver);
117
+ const podBaseUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(input.webId, this.podBaseUrlResolver, input.auth);
112
118
  const hydrated = await this.withSelectedModels(db, aiProvider, aiModel, filtered, input.webId, podBaseUrl, podFetch);
113
119
  return hydrated
114
120
  .sort(compareCredentialRecords)
@@ -135,6 +141,7 @@ class PodConnectedCredentialRepository {
135
141
  cooldownUntil: record.rateLimitResetAt,
136
142
  encryptedSecret: record.encryptedSecret,
137
143
  version: record.version,
144
+ expiresAt: record.expiresAt,
138
145
  runtimeCredential: runtimeCredentialFromMetadata({ ...record.metadata, offeringId }),
139
146
  metadata: {
140
147
  ...record.metadata,
@@ -258,7 +265,7 @@ class PodConnectedCredentialRepository {
258
265
  .filter((record) => record.webId === input.webId)
259
266
  .filter((record) => providerMatchesQuery(record.provider, input.provider, providerIds))
260
267
  .filter((record) => input.includeRevoked || record.status === 'active');
261
- const podBaseUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(input.webId, this.podBaseUrlResolver);
268
+ const podBaseUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(input.webId, this.podBaseUrlResolver, input.auth);
262
269
  return (await this.withSelectedModels(db, aiProvider, aiModel, filtered, input.webId, podBaseUrl, podFetch))
263
270
  .sort(compareCredentialRecords);
264
271
  }
@@ -331,7 +338,11 @@ class PodConnectedCredentialRepository {
331
338
  return updated !== null;
332
339
  }
333
340
  async markReauthRequired(input) {
334
- const current = await this.getActiveCredential(input);
341
+ // An explicit credentialId pins the exact row; without it the previous single-active-credential
342
+ // behaviour is preserved for callers that only know the provider.
343
+ const current = input.credentialId
344
+ ? await this.getCredentialById({ ...input, credentialId: input.credentialId })
345
+ : await this.getActiveCredential(input);
335
346
  if (!current) {
336
347
  return undefined;
337
348
  }
@@ -425,7 +436,7 @@ class PodConnectedCredentialRepository {
425
436
  }
426
437
  async dbForOwner(owner, auth) {
427
438
  const credential = (0, drizzle_solid_1.alias)(this.credentialTemplate, 'credential');
428
- const podUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(owner, this.podBaseUrlResolver);
439
+ const podUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(owner, this.podBaseUrlResolver, auth);
429
440
  const trustedFetch = await this.resolveTrustedFetch(owner, auth, podUrl);
430
441
  const podBaseUrl = podUrl.replace(/\/$/u, '');
431
442
  const settingsSparqlEndpoint = `${podBaseUrl}/settings/-/sparql`;
@@ -1841,6 +1852,13 @@ class ProviderConnectService {
1841
1852
  if ((dateFrom(secret.expiresAt)?.getTime() ?? Infinity) > Date.now()) {
1842
1853
  return { ...secret, ...metadataWithoutUndefined({ ...kimiAccountIdentityHint(input.provider, secret.accessToken) }) };
1843
1854
  }
1855
+ return this.refreshCallerOwnedSecret(secret, input);
1856
+ }
1857
+ /**
1858
+ * Exchanges the stored refresh token for a live session. Shared by import-time and use-time
1859
+ * renewal; typed reauth failures stay distinguishable so callers can mark the credential.
1860
+ */
1861
+ async refreshCallerOwnedSecret(secret, input) {
1844
1862
  if (typeof secret.refreshToken !== 'string' || !secret.refreshToken) {
1845
1863
  throw new Error('local_session_missing_refresh_token');
1846
1864
  }
@@ -1862,6 +1880,163 @@ class ProviderConnectService {
1862
1880
  }
1863
1881
  return { ...result, ...metadataWithoutUndefined({ ...kimiAccountIdentityHint(input.provider, result.accessToken) }) };
1864
1882
  }
1883
+ /**
1884
+ * Shared credential lifecycle recovery, called by the inference path when a stored OAuth session
1885
+ * is expired or was just rejected upstream.
1886
+ *
1887
+ * Renewal re-reads the credential row inside a per-credential lock (concurrent callers
1888
+ * coalesce), prefers the declared local session source when the live file still belongs to the
1889
+ * same subscriber, falls back to the stored refresh token, and persists to the SAME row through
1890
+ * the existing CAS update so user settings survive. API keys and local providers keep the
1891
+ * existing path: they report "not renewable" instead of being special-cased by provider id.
1892
+ *
1893
+ * Returns whether a retry with the now-stored credential is worthwhile.
1894
+ */
1895
+ async renewCredential(input) {
1896
+ if (!this.credentialRepository || !this.vault)
1897
+ return false;
1898
+ const provider = normalizeProvider(input.provider);
1899
+ return withCredentialRenewalLock(JSON.stringify([input.webId, input.credentialId]), async () => {
1900
+ const current = await this.credentialRepository.getCredentialById({
1901
+ webId: input.webId,
1902
+ provider,
1903
+ deployment: input.deployment,
1904
+ credentialId: input.credentialId,
1905
+ auth: input.auth,
1906
+ });
1907
+ // A revoked or deliberately disabled credential must not be renewed back into service.
1908
+ if (!current || current.status !== 'active' || current.enabled === false)
1909
+ return false;
1910
+ if (current.authMode !== 'deviceCodeOAuth')
1911
+ return false;
1912
+ const secret = await this.vault.open({ webId: input.webId }, current.credentialIri, provider, current.encryptedSecret);
1913
+ const valid = (dateFrom(secret.expiresAt)?.getTime() ?? Infinity) > Date.now();
1914
+ const versionChanged = input.observedVersion !== undefined && current.version !== input.observedVersion;
1915
+ if (valid) {
1916
+ // A concurrent caller already renewed this exact row: tell the caller to reload and retry.
1917
+ if (versionChanged)
1918
+ return true;
1919
+ // Nothing to renew for a session that is genuinely still valid.
1920
+ if (input.reason === 'expired')
1921
+ return false;
1922
+ // `authentication_failed` with a still-"valid" token: fall through and refresh it.
1923
+ }
1924
+ const offeringId = current.offeringId ?? defaultOfferingFor(provider, current.authMode);
1925
+ // Live-file adoption is reserved for credentials proven imported by the existing fingerprint
1926
+ // contract. A separately device-authorized grant must use its own refresh token instead of
1927
+ // silently inheriting whatever session the host CLI currently holds.
1928
+ const imported = typeof secret.importedSessionFingerprint === 'string';
1929
+ let next = imported
1930
+ ? await this.adoptLiveImportedSession({
1931
+ provider,
1932
+ offeringId,
1933
+ stored: secret,
1934
+ storedScopes: current.scopes,
1935
+ deployment: input.deployment,
1936
+ rejectUnchangedAccessToken: input.reason === 'authentication_failed',
1937
+ })
1938
+ : undefined;
1939
+ if (!next) {
1940
+ try {
1941
+ next = await this.refreshCallerOwnedSecret(secret, {
1942
+ webId: input.webId,
1943
+ deployment: input.deployment,
1944
+ provider,
1945
+ offeringId,
1946
+ credentialId: current.id,
1947
+ expectedVersion: current.version ?? 0,
1948
+ auth: input.auth,
1949
+ });
1950
+ }
1951
+ catch (error) {
1952
+ if (!(error instanceof Error) || error.message !== 'local_session_reauth_required') {
1953
+ return false;
1954
+ }
1955
+ await this.credentialRepository.markReauthRequired({
1956
+ webId: input.webId,
1957
+ provider,
1958
+ deployment: input.deployment,
1959
+ reason: 'invalid_grant',
1960
+ credentialId: current.id,
1961
+ expectedVersion: current.version,
1962
+ auth: input.auth,
1963
+ });
1964
+ throw new errors_1.GatewayProtocolError('Provider session requires re-authorization', {
1965
+ code: 'credential_unavailable',
1966
+ status: 401,
1967
+ details: { provider, credentialId: current.id, reauthRequired: true },
1968
+ });
1969
+ }
1970
+ }
1971
+ // A "renewal" that would re-store the very token the provider just rejected is not a renewal;
1972
+ // leave the row alone so the caller does not retry with an unchanged rejected token.
1973
+ if (input.reason === 'authentication_failed' && next.accessToken === secret.accessToken) {
1974
+ return false;
1975
+ }
1976
+ const health = current.health === 'disabled' ? 'disabled' : 'healthy';
1977
+ const updated = await this.credentialRepository.updateCredential({
1978
+ webId: input.webId,
1979
+ provider,
1980
+ deployment: input.deployment,
1981
+ credentialId: current.id,
1982
+ expectedVersion: current.version,
1983
+ auth: input.auth,
1984
+ patch: {
1985
+ encryptedSecret: await this.vault.seal({ webId: input.webId }, current.credentialIri, provider, next),
1986
+ expiresAt: dateFrom(next.expiresAt),
1987
+ scopes: scopeListFromSecret(next),
1988
+ reauthRequired: false,
1989
+ health,
1990
+ metadata: metadataWithoutUndefined({
1991
+ ...current.metadata,
1992
+ accountId: next.accountId,
1993
+ authoritativeSubject: next.accountSubject,
1994
+ offeringId,
1995
+ health,
1996
+ }),
1997
+ },
1998
+ });
1999
+ return Boolean(updated);
2000
+ });
2001
+ }
2002
+ /**
2003
+ * On-demand re-read of the declared local session source for an imported credential: the CLI
2004
+ * that owns the file may have rotated it since import. The live session is adopted only when it
2005
+ * is unexpired, still belongs to the same subscriber, and does not narrow the granted scopes the
2006
+ * stored credential relies on.
2007
+ */
2008
+ async adoptLiveImportedSession(input) {
2009
+ const importer = input.offeringId
2010
+ ? this.localSessionImporters.get(localSessionImporterKey(input.provider, input.offeringId))
2011
+ : undefined;
2012
+ if (!importer)
2013
+ return undefined;
2014
+ let imported;
2015
+ try {
2016
+ imported = await importer.importSession({ deployment: input.deployment });
2017
+ }
2018
+ catch {
2019
+ // A missing or unreadable live file must not block the stored refresh token.
2020
+ return undefined;
2021
+ }
2022
+ if ((dateFrom(imported.secret.expiresAt)?.getTime() ?? 0) <= Date.now())
2023
+ return undefined;
2024
+ // Re-reading a file that still holds the token the provider just rejected is not a renewal;
2025
+ // the caller must fall back to the declared refresh token instead.
2026
+ if (input.rejectUnchangedAccessToken && imported.secret.accessToken === input.stored.accessToken) {
2027
+ return undefined;
2028
+ }
2029
+ if (!sameImportedSession(input.provider, input.stored, imported.secret))
2030
+ return undefined;
2031
+ const liveScopes = scopeListFromSecret(imported.secret);
2032
+ if (input.storedScopes?.length
2033
+ && !input.storedScopes.every((scope) => liveScopes?.includes(scope)))
2034
+ return undefined;
2035
+ return {
2036
+ ...imported.secret,
2037
+ importedSessionFingerprint: importedSessionFingerprint(imported.secret),
2038
+ };
2039
+ }
1865
2040
  async updateCredential(input) {
1866
2041
  this.requireProvidedProvider(input.deployment, input.provider);
1867
2042
  this.assertEndpointIsProvided({
@@ -3563,6 +3738,21 @@ function offeringMatchesAuthMode(offeringAuthModes, authMode) {
3563
3738
  return offeringAuthModes.includes('local');
3564
3739
  return offeringAuthModes.includes('oauth') || offeringAuthModes.includes('deviceCode');
3565
3740
  }
3741
+ async function withCredentialRenewalLock(key, operation) {
3742
+ const previous = credentialRenewalLocks.get(key) ?? Promise.resolve();
3743
+ let release;
3744
+ const pending = new Promise((resolve) => { release = resolve; });
3745
+ credentialRenewalLocks.set(key, pending);
3746
+ await previous;
3747
+ try {
3748
+ return await operation();
3749
+ }
3750
+ finally {
3751
+ release();
3752
+ if (credentialRenewalLocks.get(key) === pending)
3753
+ credentialRenewalLocks.delete(key);
3754
+ }
3755
+ }
3566
3756
  async function updateByCredentialIdAndVersion(params) {
3567
3757
  const { owner, db, credential, credentialId, expectedVersion, patch } = params;
3568
3758
  const key = JSON.stringify([owner, credentialId]);
@@ -313,7 +313,7 @@ class PodModelSelectionRepository {
313
313
  async dbForOwner(owner, auth) {
314
314
  assertOwnerWebId(owner);
315
315
  assertAuthOwner(owner, auth);
316
- const podUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(owner, this.podBaseUrlResolver);
316
+ const podUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(owner, this.podBaseUrlResolver, auth);
317
317
  const trustedFetch = await this.podAccess?.getPodFetch(owner, {
318
318
  ...(auth ? { auth } : {}),
319
319
  podBaseUrl: podUrl,
@@ -173,6 +173,11 @@ function podAccessError(owner, auth) {
173
173
  */
174
174
  function isPodAccessFailure(message) {
175
175
  return message === 'service_access_missing'
176
+ || message === 'Authoritative Pod storage binding ambiguous'
177
+ || message === 'Authoritative Pod credential binding changed on cached context'
178
+ || message === 'Authoritative Pod credential binding changed while opening database'
179
+ || message === 'Authoritative Pod storage binding changed on cached context'
180
+ || message === 'Authoritative Pod storage binding conflicts with request'
176
181
  || message.startsWith(exports.POD_INTERFACE_KEY_MISSING)
177
182
  || message.startsWith(exports.POD_INTERFACE_KEY_REJECTED)
178
183
  || message.startsWith(CallerPodAccess_1.CALLER_DPOP_REPLAY_UNSUPPORTED)
@@ -1,2 +1,11 @@
1
- export type PodBaseUrlResolver = (webId: string) => Promise<string | undefined>;
2
- export declare function resolveOwnerPodBaseUrl(webId: string, resolver?: PodBaseUrlResolver): Promise<string>;
1
+ import type { AuthContext } from '../../auth/AuthContext';
2
+ import type { PodLookupRepository } from '../../../identity/drizzle/PodLookupRepository';
3
+ export type PodBaseUrlResolver = ((webId: string, requestedPodUrl?: string) => Promise<string | undefined>) & {
4
+ /** Canonical roots registered to the exact owner, for bounded verifier lookups only. */
5
+ ownedPodUrls?: (webId: string) => Promise<string[]>;
6
+ };
7
+ export declare function resolveOwnerPodBindings(webId: string, resolver?: PodBaseUrlResolver): Promise<string[]>;
8
+ export declare function resolveOwnerPodBaseUrl(webId: string, resolver?: PodBaseUrlResolver, auth?: AuthContext): Promise<string>;
9
+ /** Only repository-verified ownership and storage bindings may authorize a selected Pod. */
10
+ export declare function createOwnerPodBaseUrlResolver(repository: Pick<PodLookupRepository, 'findByWebId' | 'findAllByWebId'> | undefined, selection?: 'first' | 'unique'): PodBaseUrlResolver;
11
+ export declare function normalizeVerifiedPodRoot(value: string): string;
@@ -1,21 +1,83 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveOwnerPodBindings = resolveOwnerPodBindings;
3
4
  exports.resolveOwnerPodBaseUrl = resolveOwnerPodBaseUrl;
5
+ exports.createOwnerPodBaseUrlResolver = createOwnerPodBaseUrlResolver;
6
+ exports.normalizeVerifiedPodRoot = normalizeVerifiedPodRoot;
4
7
  const drizzle_solid_1 = require("@undefineds.co/drizzle-solid");
5
- async function resolveOwnerPodBaseUrl(webId, resolver) {
6
- const resolved = await resolver?.(webId);
7
- return normalizePodBaseUrl(resolved ?? (0, drizzle_solid_1.resolvePodBaseUrl)(webId));
8
+ async function resolveOwnerPodBindings(webId, resolver) {
9
+ if (!resolver)
10
+ throw new Error('service_access_missing');
11
+ if (resolver.ownedPodUrls)
12
+ return resolver.ownedPodUrls(webId);
13
+ const root = await resolver(webId);
14
+ return root ? [normalizeVerifiedPodRoot(root)] : [];
8
15
  }
9
- function normalizePodBaseUrl(value) {
16
+ async function resolveOwnerPodBaseUrl(webId, resolver, auth) {
17
+ const solidAuth = auth?.type === 'solid' ? auth : undefined;
18
+ if (solidAuth && solidAuth.webId !== webId)
19
+ throw new Error('caller_owner_mismatch');
20
+ const requested = solidAuth?.requestedPodUrl;
21
+ const authorized = solidAuth?.authorizedPodUrl;
22
+ if (requested && (solidAuth?.viaGatewayApiKey || solidAuth?.internalInvocation) && !authorized) {
23
+ throw new Error('service_access_missing');
24
+ }
25
+ const selected = requested ?? authorized;
26
+ const resolved = await resolver?.(webId, selected);
27
+ if ((resolver || selected) && !resolved)
28
+ throw new Error('service_access_missing');
29
+ const podUrl = normalizeVerifiedPodRoot(resolved ?? (0, drizzle_solid_1.resolvePodBaseUrl)(webId));
30
+ if (authorized && podUrl !== normalizeVerifiedPodRoot(authorized))
31
+ throw new Error('service_access_missing');
32
+ return podUrl;
33
+ }
34
+ /** Only repository-verified ownership and storage bindings may authorize a selected Pod. */
35
+ function createOwnerPodBaseUrlResolver(repository, selection = 'first') {
36
+ const resolver = async (webId, requestedPodUrl) => {
37
+ const pods = selection === 'unique' || requestedPodUrl !== undefined
38
+ ? await repository?.findAllByWebId(webId) ?? []
39
+ : [await repository?.findByWebId(webId)];
40
+ if (requestedPodUrl !== undefined) {
41
+ const requested = normalizeVerifiedPodRoot(requestedPodUrl);
42
+ const owned = pods.filter(pod => [pod?.storageUrl, pod?.baseUrl].some(root => root !== undefined && normalizeVerifiedPodRoot(root) === requested));
43
+ const roots = new Set(owned.flatMap(pod => {
44
+ const root = pod?.storageUrl ?? pod?.baseUrl;
45
+ return root ? [normalizeVerifiedPodRoot(root)] : [];
46
+ }));
47
+ if (roots.size > 1)
48
+ throw new Error('Authoritative Pod storage binding ambiguous');
49
+ return roots.values().next().value;
50
+ }
51
+ const roots = pods.flatMap(pod => {
52
+ const root = pod?.storageUrl ?? pod?.baseUrl;
53
+ return root ? [root] : [];
54
+ });
55
+ if (selection === 'first')
56
+ return roots[0];
57
+ const normalized = new Set(roots.map(normalizeVerifiedPodRoot));
58
+ if (normalized.size > 1)
59
+ throw new Error('Authoritative Pod storage binding ambiguous');
60
+ return normalized.values().next().value;
61
+ };
62
+ resolver.ownedPodUrls = async (webId) => {
63
+ const pods = await repository?.findAllByWebId(webId) ?? [];
64
+ return [...new Set(pods.flatMap(pod => {
65
+ const root = pod.storageUrl ?? pod.baseUrl;
66
+ return root ? [normalizeVerifiedPodRoot(root)] : [];
67
+ }))];
68
+ };
69
+ return resolver;
70
+ }
71
+ function normalizeVerifiedPodRoot(value) {
10
72
  const url = new URL(value);
11
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
12
- throw new TypeError('Pod storage URL must use http or https');
73
+ if (value !== value.trim() || /[\s\\?#]/u.test(value)
74
+ || !['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
75
+ throw new Error('Invalid Pod storage binding');
13
76
  }
14
- url.hash = '';
15
- url.search = '';
16
- if (!url.pathname.endsWith('/')) {
77
+ if (!url.pathname.endsWith('/'))
17
78
  url.pathname += '/';
18
- }
79
+ if (url.href !== (value.endsWith('/') ? value : `${value}/`))
80
+ throw new Error('Invalid Pod storage binding');
19
81
  return url.href;
20
82
  }
21
83
  //# sourceMappingURL=PodBaseUrlResolver.js.map
@@ -398,7 +398,7 @@ class PodQuotaSnapshotRepository {
398
398
  return { ...snapshot, stale: false };
399
399
  }
400
400
  async dbForOwner(owner, auth) {
401
- const podUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(owner, this.podBaseUrlResolver);
401
+ const podUrl = await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(owner, this.podBaseUrlResolver, auth);
402
402
  const trustedFetch = await this.resolveTrustedFetch(owner, auth, podUrl);
403
403
  const db = await this.dbFactory({ owner, auth, fetch: trustedFetch, podUrl });
404
404
  await db.init?.(models_1.quotaSnapshotResource);
@@ -135,6 +135,13 @@ export declare class ModelRouter {
135
135
  private selectExplicitCredential;
136
136
  private selectCredential;
137
137
  private isCredentialUsable;
138
+ /**
139
+ * Whether a credential candidate is currently eligible on its own availability signals:
140
+ * enabled, healthy and not quota-exhausted. Routing, model visibility and the gateway's
141
+ * post-renewal reload share this predicate so a row disabled or marked reauth-required between
142
+ * a credential lifecycle write and its next use is never handed to the provider runtime.
143
+ */
144
+ isCredentialEligible(candidate: GatewayCredentialCandidate): boolean;
138
145
  private effectiveCooldownUntil;
139
146
  private credentialMatchesProvider;
140
147
  private routeProviderIdForCredential;
@@ -426,13 +426,7 @@ class ModelRouter {
426
426
  return usable[0];
427
427
  }
428
428
  async isCredentialUsable(input, candidate, model) {
429
- if (!candidate.enabled) {
430
- return false;
431
- }
432
- if (candidate.health && candidate.health !== 'healthy') {
433
- return false;
434
- }
435
- if (candidate.quota?.status === 'exhausted') {
429
+ if (!this.isCredentialEligible(candidate)) {
436
430
  return false;
437
431
  }
438
432
  const cooldownUntil = await this.effectiveCooldownUntil(input, candidate);
@@ -441,6 +435,17 @@ class ModelRouter {
441
435
  }
442
436
  return credentialSupportsModel(candidate, model);
443
437
  }
438
+ /**
439
+ * Whether a credential candidate is currently eligible on its own availability signals:
440
+ * enabled, healthy and not quota-exhausted. Routing, model visibility and the gateway's
441
+ * post-renewal reload share this predicate so a row disabled or marked reauth-required between
442
+ * a credential lifecycle write and its next use is never handed to the provider runtime.
443
+ */
444
+ isCredentialEligible(candidate) {
445
+ return candidate.enabled
446
+ && (!candidate.health || candidate.health === 'healthy')
447
+ && candidate.quota?.status !== 'exhausted';
448
+ }
444
449
  async effectiveCooldownUntil(input, candidate) {
445
450
  const storedCooldown = await this.affinityStore.getCooldown({
446
451
  deployment: input.deployment,
@@ -545,9 +550,7 @@ class ModelRouter {
545
550
  return models;
546
551
  }
547
552
  isCredentialModelVisible(candidate) {
548
- return candidate.enabled
549
- && (!candidate.health || candidate.health === 'healthy')
550
- && candidate.quota?.status !== 'exhausted'
553
+ return this.isCredentialEligible(candidate)
551
554
  && (!candidate.cooldownUntil || candidate.cooldownUntil.getTime() <= this.now().getTime());
552
555
  }
553
556
  /**
@@ -17,7 +17,7 @@ export interface AiConnectionsServiceAccessDescriptor {
17
17
  resources: AiConnectionsServiceAccessResource[];
18
18
  }
19
19
  export interface AiConnectionsServiceAccessResource {
20
- id: 'providerCredentials' | 'providerDefinitions' | 'gatewayAccessKeys' | 'gatewayAccessKeySecrets' | 'quotaSnapshots' | `providerDocument:${string}`;
20
+ id: 'providerCredentials' | 'providerDefinitions' | 'quotaSnapshots' | `providerDocument:${string}`;
21
21
  url: string;
22
22
  mediaType: 'text/turtle' | 'application/json';
23
23
  access: {
@@ -33,7 +33,3 @@ export declare function createAiConnectionsServiceAccess(input: {
33
33
  serviceWebId: string;
34
34
  podBaseUrl?: string;
35
35
  }): AiConnectionsServiceAccessDescriptor;
36
- export declare function resolveGatewayAccessKeyResourceUrl(ownerWebId: string, podBaseUrl?: string): string;
37
- export declare function resolveGatewayAccessKeySecretResourceUrl(ownerWebId: string, podBaseUrl?: string): string;
38
- export declare function resolveGatewayAccessKeySparqlEndpoint(ownerWebId: string, podBaseUrl?: string): string;
39
- export declare function isGatewayAccessKeySparqlEndpoint(ownerWebId: string, resourceUrlValue: URL | string): boolean;