@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.
- package/dist/api/ApiServer.js +4 -0
- package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
- package/dist/api/ai-gateway/AiGatewayService.js +145 -6
- package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
- package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
- package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
- package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
- package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
- package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
- package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
- package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
- package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
- package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
- package/dist/api/ai-gateway/connect/index.d.ts +40 -0
- package/dist/api/ai-gateway/connect/index.js +194 -4
- package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
- package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
- package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
- package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
- package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
- package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
- package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
- package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
- package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
- package/dist/api/auth/AuthContext.d.ts +4 -0
- package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
- package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
- package/dist/api/auth/SolidSessionFactory.js +114 -4
- package/dist/api/chatkit/pod-store.d.ts +8 -1
- package/dist/api/chatkit/pod-store.js +54 -3
- package/dist/api/container/common.js +34 -60
- package/dist/api/container/index.js +0 -3
- package/dist/api/container/routes.js +1 -8
- package/dist/api/container/types.d.ts +2 -9
- package/dist/api/container/types.js +0 -5
- package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
- package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
- package/dist/api/middleware/AuthMiddleware.js +29 -1
- package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
- package/dist/cli/index.js +2 -1
- package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
- package/dist/runtime/ingress-port.d.ts +9 -2
- package/dist/runtime/ingress-port.js +7 -2
- package/dist/runtime/local-secret-file.d.ts +1 -1
- package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
- package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
- package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
- package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
- package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
- package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
- package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
- package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
- package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
- package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
- package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
- package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
- package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
- package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
- package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
- package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
- package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
- package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
- package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
- package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
- package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
- package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
- package/package.json +2 -2
- package/static/app/assets/main.css +1 -1
- package/static/app/assets/main.js +18 -18
- package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
- package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
- package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
- package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
- package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
- package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
- package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
- package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
- package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
- package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
- package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
- package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
- package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
- package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
- package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
- package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
- package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
- package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
- package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
- package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
- package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
- package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
- package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
- package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
- package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
- package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
- package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
- package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
- package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
- package/static/dashboard/dashboard.html +2 -2
- package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
- package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
- package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
- package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
- package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
- package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
- package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
- package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
- package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
- package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
- package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
- package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
- package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
- package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
- package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
- package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
- package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
- package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
- package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
- package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
- package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
- package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
- package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
- package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
- package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
- package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
- package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
- package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
- package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
- package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
- package/static/settings/auth-callback.html +3 -3
- package/static/settings/settings.html +3 -3
- package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
- package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
- package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
- package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
- package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
- package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
- package/dist/runtime/gateway-locator-secret.d.ts +0 -5
- package/dist/runtime/gateway-locator-secret.js +0 -30
- package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
- package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
- package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
- package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
- package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
- package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
- package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
- package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
- package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
- package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
- package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
- package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
- package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
- package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
- 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
|
|
25
|
+
const filePath = this.profile.resolvePath(this.homeDir, this.env);
|
|
26
|
+
const raw = await this.readSessionFile(filePath);
|
|
24
27
|
const payload = parseJsonObject(raw);
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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 (
|
|
12
|
-
|
|
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.
|
|
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
|
|
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
|
|
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' | '
|
|
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;
|