@undefineds.co/xpod 0.4.29 → 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
|
@@ -2,21 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AI_CONNECTIONS_PROVIDER_DOCUMENT_IDS = exports.AI_CONNECTIONS_APPLET_ID = void 0;
|
|
4
4
|
exports.createAiConnectionsServiceAccess = createAiConnectionsServiceAccess;
|
|
5
|
-
exports.resolveGatewayAccessKeyResourceUrl = resolveGatewayAccessKeyResourceUrl;
|
|
6
|
-
exports.resolveGatewayAccessKeySecretResourceUrl = resolveGatewayAccessKeySecretResourceUrl;
|
|
7
|
-
exports.resolveGatewayAccessKeySparqlEndpoint = resolveGatewayAccessKeySparqlEndpoint;
|
|
8
|
-
exports.isGatewayAccessKeySparqlEndpoint = isGatewayAccessKeySparqlEndpoint;
|
|
9
5
|
const drizzle_solid_1 = require("@undefineds.co/drizzle-solid");
|
|
10
6
|
const provider_catalog_1 = require("@undefineds.co/ai-connections/provider-catalog");
|
|
11
7
|
Object.defineProperty(exports, "AI_CONNECTIONS_PROVIDER_DOCUMENT_IDS", { enumerable: true, get: function () { return provider_catalog_1.AI_CONNECTIONS_PROVIDER_DOCUMENT_IDS; } });
|
|
12
|
-
const
|
|
8
|
+
const service_access_resources_1 = require("@undefineds.co/ai-connections/service-access-resources");
|
|
13
9
|
exports.AI_CONNECTIONS_APPLET_ID = 'co.undefineds.ai-connections';
|
|
14
|
-
const declaredResourceBases = new WeakMap([
|
|
15
|
-
[models_1.credentialResource, declaredResourceBase(models_1.credentialResource)],
|
|
16
|
-
[models_1.aiProviderResource, declaredResourceBase(models_1.aiProviderResource)],
|
|
17
|
-
[models_1.gatewayAccessKeyResource, declaredResourceBase(models_1.gatewayAccessKeyResource)],
|
|
18
|
-
[models_1.quotaSnapshotResource, declaredResourceBase(models_1.quotaSnapshotResource)],
|
|
19
|
-
]);
|
|
20
10
|
function createAiConnectionsServiceAccess(input) {
|
|
21
11
|
return {
|
|
22
12
|
appletId: exports.AI_CONNECTIONS_APPLET_ID,
|
|
@@ -24,62 +14,17 @@ function createAiConnectionsServiceAccess(input) {
|
|
|
24
14
|
webId: input.serviceWebId,
|
|
25
15
|
label: 'Xpod AI Connection',
|
|
26
16
|
},
|
|
27
|
-
resources:
|
|
28
|
-
['providerCredentials', resourceUrl(input.ownerWebId, models_1.credentialResource, input.podBaseUrl)],
|
|
29
|
-
['providerDefinitions', resourceUrl(input.ownerWebId, models_1.aiProviderResource, input.podBaseUrl)],
|
|
30
|
-
['gatewayAccessKeys', resolveGatewayAccessKeyResourceUrl(input.ownerWebId, input.podBaseUrl)],
|
|
31
|
-
['gatewayAccessKeySecrets', resolveGatewayAccessKeySecretResourceUrl(input.ownerWebId, input.podBaseUrl), 'application/json'],
|
|
32
|
-
['quotaSnapshots', resourceUrl(input.ownerWebId, models_1.quotaSnapshotResource, input.podBaseUrl)],
|
|
33
|
-
...provider_catalog_1.AI_CONNECTIONS_PROVIDER_DOCUMENT_IDS.map((provider) => [
|
|
34
|
-
`providerDocument:${provider}`,
|
|
35
|
-
providerDocumentUrl(input.ownerWebId, provider, input.podBaseUrl),
|
|
36
|
-
]),
|
|
37
|
-
].map(([id, url, mediaType]) => ({
|
|
17
|
+
resources: service_access_resources_1.AI_CONNECTIONS_SERVICE_RESOURCE_IDS.map(id => ({
|
|
38
18
|
id,
|
|
39
|
-
|
|
40
|
-
mediaType: mediaType ?? 'text/turtle',
|
|
19
|
+
...requiredResourceLocation(id, input.ownerWebId, input.podBaseUrl),
|
|
41
20
|
access: { read: true, append: true, write: true },
|
|
42
21
|
})),
|
|
43
22
|
};
|
|
44
23
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return new URL('.data/ai/gateway/access-key-secrets.json', podRoot).href;
|
|
51
|
-
}
|
|
52
|
-
function resolveGatewayAccessKeySparqlEndpoint(ownerWebId, podBaseUrl) {
|
|
53
|
-
return `${resolveGatewayAccessKeyResourceUrl(ownerWebId, podBaseUrl).replace(/\/$/u, '')}/-/sparql`;
|
|
54
|
-
}
|
|
55
|
-
function isGatewayAccessKeySparqlEndpoint(ownerWebId, resourceUrlValue) {
|
|
56
|
-
try {
|
|
57
|
-
const resource = typeof resourceUrlValue === 'string' ? new URL(resourceUrlValue) : resourceUrlValue;
|
|
58
|
-
const endpoint = new URL(resolveGatewayAccessKeySparqlEndpoint(ownerWebId));
|
|
59
|
-
return resource.origin === endpoint.origin && resource.pathname === endpoint.pathname;
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function resourceUrl(ownerWebId, resource, podBaseUrl) {
|
|
66
|
-
const podRoot = `${(podBaseUrl ?? (0, drizzle_solid_1.resolvePodBaseUrl)(ownerWebId)).replace(/\/$/u, '')}/`;
|
|
67
|
-
const resourcePath = declaredResourceBases.get(resource);
|
|
68
|
-
if (!resourcePath) {
|
|
69
|
-
throw new Error('AI Connection resource is missing an immutable declared base');
|
|
70
|
-
}
|
|
71
|
-
const documentPath = resource.buildId({ id: '__service_access__' }).split('#')[0];
|
|
72
|
-
return new URL(`${resourcePath}/${documentPath}`.replace(/^\/+/u, ''), podRoot).href;
|
|
73
|
-
}
|
|
74
|
-
function providerDocumentUrl(ownerWebId, provider, podBaseUrl) {
|
|
75
|
-
const podRoot = `${(podBaseUrl ?? (0, drizzle_solid_1.resolvePodBaseUrl)(ownerWebId)).replace(/\/$/u, '')}/`;
|
|
76
|
-
return new URL(`settings/providers/${provider}.ttl`, podRoot).href;
|
|
77
|
-
}
|
|
78
|
-
function declaredResourceBase(resource) {
|
|
79
|
-
const base = resource.config?.base?.replace(/^\/+|\/+$/gu, '');
|
|
80
|
-
if (!base) {
|
|
81
|
-
throw new Error('AI Connection resource is missing a declared base');
|
|
82
|
-
}
|
|
83
|
-
return base;
|
|
24
|
+
function requiredResourceLocation(id, ownerWebId, podBaseUrl) {
|
|
25
|
+
const location = (0, service_access_resources_1.resolveAiConnectionsServiceResource)(id, podBaseUrl ?? (0, drizzle_solid_1.resolvePodBaseUrl)(ownerWebId));
|
|
26
|
+
if (!location)
|
|
27
|
+
throw new Error('AI Connection resource is not declared');
|
|
28
|
+
return location;
|
|
84
29
|
}
|
|
85
30
|
//# sourceMappingURL=AiConnectionsServiceAccess.js.map
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export interface SolidAuthContext {
|
|
11
11
|
type: 'solid';
|
|
12
|
+
/** Untrusted current-Pod selection hint; only the shared storage adapter may verify it. */
|
|
13
|
+
requestedPodUrl?: string;
|
|
14
|
+
/** Trusted Pod capability scope, from authenticated signed claims or a verified key DB. */
|
|
15
|
+
authorizedPodUrl?: string;
|
|
12
16
|
webId: string;
|
|
13
17
|
accountId?: string;
|
|
14
18
|
clientId?: string;
|
|
@@ -72,19 +72,25 @@ class ClientCredentialsAuthenticator {
|
|
|
72
72
|
// Non sk- format is intentionally unsupported; Xpod does not keep an API key mirror table.
|
|
73
73
|
return { success: false, error: 'Invalid client credentials wrapper: must start with sk-' };
|
|
74
74
|
}
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// Admission revalidates the wrapper against the issuer that owns it, on every request: a
|
|
76
|
+
// session cached for a previous request is not proof that this credential is still
|
|
77
|
+
// registered. The factory publishes the fresh session, so this same request's outbound Pod
|
|
78
|
+
// access reuses that token and its DPoP key instead of exchanging a second time.
|
|
77
79
|
let session;
|
|
78
80
|
try {
|
|
79
|
-
session = await this.sessions.
|
|
81
|
+
session = await this.sessions.admit({ clientId, clientSecret });
|
|
80
82
|
}
|
|
81
83
|
catch (error) {
|
|
82
84
|
const status = error instanceof SolidSessionFactory_1.SolidSessionError ? error.status : undefined;
|
|
83
|
-
|
|
85
|
+
// The issuer being unreachable, failing, or rate limiting us is not a statement about the
|
|
86
|
+
// credential. Only a definitive refusal (the 400/401/403 the factory treats as such) is an
|
|
87
|
+
// invalid credential; everything else is retryable availability, so it is reported as 503
|
|
88
|
+
// rather than telling the caller its key is dead.
|
|
89
|
+
const unavailable = status === undefined || status >= 500 || status === 429;
|
|
84
90
|
this.logger.warn(`Client credentials exchange failed for ${clientId.slice(0, 8)}...: ${String(error)}`);
|
|
85
91
|
return unavailable
|
|
86
92
|
? { success: false, error: 'Token exchange temporarily unavailable', category: 'service_unavailable', statusCode: 503, cause: error }
|
|
87
|
-
: { success: false, error: `Token exchange failed: ${status ?? 'unknown'}`, cause: error };
|
|
93
|
+
: { success: false, error: `Token exchange failed: ${status ?? 'unknown'}`, category: 'invalid_credentials', statusCode: 401, cause: error };
|
|
88
94
|
}
|
|
89
95
|
if (!session.webId) {
|
|
90
96
|
return { success: false, error: 'Could not determine webId from token response' };
|
|
@@ -34,11 +34,14 @@ export interface SolidSessionFactoryOptions {
|
|
|
34
34
|
maxEntries?: number;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Exchanges CSS client credentials for Solid tokens
|
|
37
|
+
* Exchanges CSS client credentials for Solid tokens.
|
|
38
38
|
*
|
|
39
|
-
* Inbound
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* Inbound admission is a fresh exchange with the issuer on every request: a session in this cache
|
|
40
|
+
* proves only that some earlier exchange succeeded, never that the credential is still
|
|
41
|
+
* registered. The cache serves the outbound side - one request that arrives with an `sk-` wrapper
|
|
42
|
+
* and then reads the Pod reuses the token its own admission just obtained instead of exchanging
|
|
43
|
+
* the same credential a second time, and repeated Pod work keeps a live token and the key it is
|
|
44
|
+
* bound to until it is spent. Cache identity is the issuer, the complete credential and its
|
|
42
45
|
* version - never just the owner or the client id, which are public identification rather than
|
|
43
46
|
* authentication evidence.
|
|
44
47
|
*/
|
|
@@ -53,9 +56,40 @@ export declare class SolidSessionFactory {
|
|
|
53
56
|
/** Cache keys per client id, so a revoked credential can be forgotten without its secret. */
|
|
54
57
|
private readonly keysByClientId;
|
|
55
58
|
private readonly clientIdByKey;
|
|
59
|
+
/**
|
|
60
|
+
* Monotonic per-client invalidation counter. `admit` reads it before its exchange and only
|
|
61
|
+
* publishes the result while it is unchanged, so a success that resolves after a revocation
|
|
62
|
+
* cannot hand the outbound cache authority the revocation just took away. It is not a
|
|
63
|
+
* revocation record and never admits anything: every request still proves its own credential
|
|
64
|
+
* with a fresh exchange.
|
|
65
|
+
*/
|
|
66
|
+
private readonly invalidationSeq;
|
|
67
|
+
/** In-flight `admit` calls per client id, so a counter is kept only while it guards one. */
|
|
68
|
+
private readonly admissionsInFlight;
|
|
56
69
|
constructor(options: SolidSessionFactoryOptions);
|
|
57
70
|
/** A usable session for this credential, exchanging it when the cached one is spent. */
|
|
58
71
|
session(credential: SolidClientCredential): Promise<SolidSession>;
|
|
72
|
+
/**
|
|
73
|
+
* Admit an inbound request that presents these client credentials.
|
|
74
|
+
*
|
|
75
|
+
* Admission is always a fresh exchange with the issuer that owns the credential. A cached token
|
|
76
|
+
* proves that some earlier exchange succeeded; it is not evidence that the credential is still
|
|
77
|
+
* registered, so it must never stand in for present authority. A credential deleted from its
|
|
78
|
+
* Account has to stop opening the gateway on the very next request, not whenever the last token
|
|
79
|
+
* it obtained happens to expire.
|
|
80
|
+
*
|
|
81
|
+
* The fresh session is published to the cache, so the rest of this request - reaching the
|
|
82
|
+
* owner's Pod - finds that same token and the DPoP key it is bound to instead of exchanging the
|
|
83
|
+
* same credential a second time. Concurrent inbound requests deliberately do not share each
|
|
84
|
+
* other's exchange, for the same reason they do not share a cached token: each has to prove the
|
|
85
|
+
* credential itself.
|
|
86
|
+
*
|
|
87
|
+
* The publishing is conditional on the credential not having been invalidated while the
|
|
88
|
+
* exchange was in flight. A success the issuer returned before a revocation is not present
|
|
89
|
+
* authority, so it is handed to this caller - already-authorized work is not torn down - but it
|
|
90
|
+
* is not written into the cache that later Pod access reads.
|
|
91
|
+
*/
|
|
92
|
+
admit(credential: SolidClientCredential): Promise<SolidSession>;
|
|
59
93
|
/**
|
|
60
94
|
* Forget every cached session for one client.
|
|
61
95
|
*
|
|
@@ -64,6 +98,18 @@ export declare class SolidSessionFactory {
|
|
|
64
98
|
* authenticating the wrapper until it expires, and a deleted API Key looks like it still works.
|
|
65
99
|
*/
|
|
66
100
|
invalidateClientCredential(clientId: string): void;
|
|
101
|
+
/**
|
|
102
|
+
* Mark a credential as definitively invalidated without deleting anything yet: the counter makes
|
|
103
|
+
* an in-flight exchange that resolves later decline to publish its result. Removed as soon as no
|
|
104
|
+
* admission for the client is still in flight, so it carries no state past its purpose.
|
|
105
|
+
*/
|
|
106
|
+
private bumpInvalidation;
|
|
107
|
+
private releaseAdmission;
|
|
108
|
+
/**
|
|
109
|
+
* Drop the counter once no admission can still be racing it. While one is in flight the counter
|
|
110
|
+
* has to survive so that admission declines to republish; afterwards it is dead weight.
|
|
111
|
+
*/
|
|
112
|
+
private pruneInvalidation;
|
|
67
113
|
private remember;
|
|
68
114
|
/** Drop one cache entry and its client-id index, whichever path removed it. */
|
|
69
115
|
private forgetKey;
|
|
@@ -10,11 +10,14 @@ const TOKEN_EXPIRY_SKEW_MS = 30_000;
|
|
|
10
10
|
const DEFAULT_TOKEN_LIFETIME_SECONDS = 300;
|
|
11
11
|
const DEFAULT_MAX_ENTRIES = 256;
|
|
12
12
|
/**
|
|
13
|
-
* Exchanges CSS client credentials for Solid tokens
|
|
13
|
+
* Exchanges CSS client credentials for Solid tokens.
|
|
14
14
|
*
|
|
15
|
-
* Inbound
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Inbound admission is a fresh exchange with the issuer on every request: a session in this cache
|
|
16
|
+
* proves only that some earlier exchange succeeded, never that the credential is still
|
|
17
|
+
* registered. The cache serves the outbound side - one request that arrives with an `sk-` wrapper
|
|
18
|
+
* and then reads the Pod reuses the token its own admission just obtained instead of exchanging
|
|
19
|
+
* the same credential a second time, and repeated Pod work keeps a live token and the key it is
|
|
20
|
+
* bound to until it is spent. Cache identity is the issuer, the complete credential and its
|
|
18
21
|
* version - never just the owner or the client id, which are public identification rather than
|
|
19
22
|
* authentication evidence.
|
|
20
23
|
*/
|
|
@@ -26,6 +29,16 @@ class SolidSessionFactory {
|
|
|
26
29
|
/** Cache keys per client id, so a revoked credential can be forgotten without its secret. */
|
|
27
30
|
this.keysByClientId = new Map();
|
|
28
31
|
this.clientIdByKey = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Monotonic per-client invalidation counter. `admit` reads it before its exchange and only
|
|
34
|
+
* publishes the result while it is unchanged, so a success that resolves after a revocation
|
|
35
|
+
* cannot hand the outbound cache authority the revocation just took away. It is not a
|
|
36
|
+
* revocation record and never admits anything: every request still proves its own credential
|
|
37
|
+
* with a fresh exchange.
|
|
38
|
+
*/
|
|
39
|
+
this.invalidationSeq = new Map();
|
|
40
|
+
/** In-flight `admit` calls per client id, so a counter is kept only while it guards one. */
|
|
41
|
+
this.admissionsInFlight = new Map();
|
|
29
42
|
this.route = (0, TokenEndpointRoute_1.resolveTokenEndpointRoute)(options.tokenEndpoint, options.publicBaseUrl);
|
|
30
43
|
this.fetchImpl = options.fetch ?? fetch;
|
|
31
44
|
this.now = options.now ?? Date.now;
|
|
@@ -58,6 +71,59 @@ class SolidSessionFactory {
|
|
|
58
71
|
this.pending.set(key, pending);
|
|
59
72
|
return promise;
|
|
60
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Admit an inbound request that presents these client credentials.
|
|
76
|
+
*
|
|
77
|
+
* Admission is always a fresh exchange with the issuer that owns the credential. A cached token
|
|
78
|
+
* proves that some earlier exchange succeeded; it is not evidence that the credential is still
|
|
79
|
+
* registered, so it must never stand in for present authority. A credential deleted from its
|
|
80
|
+
* Account has to stop opening the gateway on the very next request, not whenever the last token
|
|
81
|
+
* it obtained happens to expire.
|
|
82
|
+
*
|
|
83
|
+
* The fresh session is published to the cache, so the rest of this request - reaching the
|
|
84
|
+
* owner's Pod - finds that same token and the DPoP key it is bound to instead of exchanging the
|
|
85
|
+
* same credential a second time. Concurrent inbound requests deliberately do not share each
|
|
86
|
+
* other's exchange, for the same reason they do not share a cached token: each has to prove the
|
|
87
|
+
* credential itself.
|
|
88
|
+
*
|
|
89
|
+
* The publishing is conditional on the credential not having been invalidated while the
|
|
90
|
+
* exchange was in flight. A success the issuer returned before a revocation is not present
|
|
91
|
+
* authority, so it is handed to this caller - already-authorized work is not torn down - but it
|
|
92
|
+
* is not written into the cache that later Pod access reads.
|
|
93
|
+
*/
|
|
94
|
+
async admit(credential) {
|
|
95
|
+
const key = cacheKey(this.route, credential);
|
|
96
|
+
const clientId = credential.clientId;
|
|
97
|
+
const capturedSeq = this.invalidationSeq.get(clientId) ?? 0;
|
|
98
|
+
this.admissionsInFlight.set(clientId, (this.admissionsInFlight.get(clientId) ?? 0) + 1);
|
|
99
|
+
let session;
|
|
100
|
+
try {
|
|
101
|
+
session = await this.exchange(credential);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// A definitive refusal means this credential is gone, so the session cached under it is
|
|
105
|
+
// stale evidence and goes with it. An outage says nothing about the credential, so the
|
|
106
|
+
// cache is left alone and the caller hears 503 instead of 401.
|
|
107
|
+
try {
|
|
108
|
+
if (isCredentialRefusal(error)) {
|
|
109
|
+
this.invalidate(credential);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this.releaseAdmission(clientId);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
if ((this.invalidationSeq.get(clientId) ?? 0) === capturedSeq) {
|
|
119
|
+
this.remember(key, clientId, session);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
this.releaseAdmission(clientId);
|
|
124
|
+
}
|
|
125
|
+
return session;
|
|
126
|
+
}
|
|
61
127
|
/**
|
|
62
128
|
* Forget every cached session for one client.
|
|
63
129
|
*
|
|
@@ -66,6 +132,7 @@ class SolidSessionFactory {
|
|
|
66
132
|
* authenticating the wrapper until it expires, and a deleted API Key looks like it still works.
|
|
67
133
|
*/
|
|
68
134
|
invalidateClientCredential(clientId) {
|
|
135
|
+
this.bumpInvalidation(clientId);
|
|
69
136
|
const keys = this.keysByClientId.get(clientId);
|
|
70
137
|
for (const key of keys ?? []) {
|
|
71
138
|
this.forgetKey(key);
|
|
@@ -75,6 +142,35 @@ class SolidSessionFactory {
|
|
|
75
142
|
this.pending.delete(key);
|
|
76
143
|
}
|
|
77
144
|
}
|
|
145
|
+
this.pruneInvalidation(clientId);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Mark a credential as definitively invalidated without deleting anything yet: the counter makes
|
|
149
|
+
* an in-flight exchange that resolves later decline to publish its result. Removed as soon as no
|
|
150
|
+
* admission for the client is still in flight, so it carries no state past its purpose.
|
|
151
|
+
*/
|
|
152
|
+
bumpInvalidation(clientId) {
|
|
153
|
+
this.invalidationSeq.set(clientId, (this.invalidationSeq.get(clientId) ?? 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
releaseAdmission(clientId) {
|
|
156
|
+
const remaining = (this.admissionsInFlight.get(clientId) ?? 1) - 1;
|
|
157
|
+
if (remaining > 0) {
|
|
158
|
+
this.admissionsInFlight.set(clientId, remaining);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.admissionsInFlight.delete(clientId);
|
|
162
|
+
}
|
|
163
|
+
this.pruneInvalidation(clientId);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Drop the counter once no admission can still be racing it. While one is in flight the counter
|
|
167
|
+
* has to survive so that admission declines to republish; afterwards it is dead weight.
|
|
168
|
+
*/
|
|
169
|
+
pruneInvalidation(clientId) {
|
|
170
|
+
if (this.admissionsInFlight.has(clientId)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.invalidationSeq.delete(clientId);
|
|
78
174
|
}
|
|
79
175
|
remember(key, clientId, session) {
|
|
80
176
|
this.sessions.set(key, session);
|
|
@@ -113,8 +209,10 @@ class SolidSessionFactory {
|
|
|
113
209
|
if (expectedSession && this.sessions.get(key) !== expectedSession) {
|
|
114
210
|
return;
|
|
115
211
|
}
|
|
212
|
+
this.bumpInvalidation(credential.clientId);
|
|
116
213
|
this.pending.delete(key);
|
|
117
214
|
this.forgetKey(key);
|
|
215
|
+
this.pruneInvalidation(credential.clientId);
|
|
118
216
|
}
|
|
119
217
|
async exchange(credential) {
|
|
120
218
|
const dpopKey = await (0, solid_client_authn_core_1.generateDpopKeyPair)();
|
|
@@ -172,6 +270,18 @@ function cacheKey(route, credential) {
|
|
|
172
270
|
function fingerprintSecret(secret) {
|
|
173
271
|
return (0, node_crypto_1.createHash)('sha256').update(secret).digest('hex');
|
|
174
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Whether the issuer definitively refused the credential itself, rather than failing to answer.
|
|
275
|
+
*
|
|
276
|
+
* Only 400, 401 and 403 mean "this credential is not registered any more". Everything else is not
|
|
277
|
+
* evidence about the credential: a 5xx, a 429, a network error or an unparseable response is an
|
|
278
|
+
* outage or a rate limit of the issuer, and reporting that as a revoked key would both mislead the
|
|
279
|
+
* caller and throw away a session that is still perfectly good.
|
|
280
|
+
*/
|
|
281
|
+
function isCredentialRefusal(error) {
|
|
282
|
+
const status = error instanceof SolidSessionError ? error.status : undefined;
|
|
283
|
+
return status === 400 || status === 401 || status === 403;
|
|
284
|
+
}
|
|
175
285
|
function pruneOldest(cache, limit, onEvict) {
|
|
176
286
|
while (cache.size > limit) {
|
|
177
287
|
const oldest = cache.keys().next();
|
|
@@ -5,7 +5,7 @@ import { type RunListOptions, type RunRecordData, type RunStepRecordData, type R
|
|
|
5
5
|
import { type TaskListOptions, type TaskRecordData, type TaskStore } from '../tasks/store';
|
|
6
6
|
import { type TaskAuthBindingRepository } from '../tasks/TaskAuthBinding';
|
|
7
7
|
import { type PodAccessFetchProvider } from '../ai-gateway/pod/OwnerPodAccess';
|
|
8
|
-
import type
|
|
8
|
+
import { type PodBaseUrlResolver } from '../ai-gateway/pod/PodBaseUrlResolver';
|
|
9
9
|
import { type AiCredentialSecretDecoder } from '../../ai/service/AiCredentialSecret';
|
|
10
10
|
import { CredentialStatus } from '../../credential/schema/types';
|
|
11
11
|
import { type ServerGroupReconcilerService } from '../reconciler';
|
|
@@ -59,6 +59,13 @@ export declare class PodChatKitStore implements ChatKitStore<StoreContext>, RunS
|
|
|
59
59
|
*/
|
|
60
60
|
private getDb;
|
|
61
61
|
private openDb;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the request's authoritative Pod root, or `undefined` when this deployment has no
|
|
64
|
+
* binding resolver to consult. Reporting a missing binding is left to the caller so the
|
|
65
|
+
* credential path can be classified first.
|
|
66
|
+
*/
|
|
67
|
+
private resolveContextPodBaseUrl;
|
|
68
|
+
private requireContextPodBaseUrl;
|
|
62
69
|
/** Stable identity of the credential binding behind a cached database, without secrets. */
|
|
63
70
|
private podCredentialBinding;
|
|
64
71
|
/**
|
|
@@ -28,6 +28,7 @@ const store_2 = require("../tasks/store");
|
|
|
28
28
|
const TaskAuthBinding_1 = require("../tasks/TaskAuthBinding");
|
|
29
29
|
const CallerPodAccess_1 = require("../ai-gateway/auth/CallerPodAccess");
|
|
30
30
|
const OwnerPodAccess_1 = require("../ai-gateway/pod/OwnerPodAccess");
|
|
31
|
+
const PodBaseUrlResolver_1 = require("../ai-gateway/pod/PodBaseUrlResolver");
|
|
31
32
|
const AuthContext_1 = require("../auth/AuthContext");
|
|
32
33
|
const provider_1 = require("../../ai/schema/provider");
|
|
33
34
|
const model_1 = require("../../ai/schema/model");
|
|
@@ -114,6 +115,13 @@ class PodChatKitStore {
|
|
|
114
115
|
if (expectedAuth && expectedAuth !== this.podCredentialBinding(context)) {
|
|
115
116
|
throw new Error('Authoritative Pod credential binding changed on cached context');
|
|
116
117
|
}
|
|
118
|
+
const auth = context.auth;
|
|
119
|
+
if (auth?.type === 'solid' && (auth.requestedPodUrl || auth.authorizedPodUrl)) {
|
|
120
|
+
const selectedRoot = await this.requireContextPodBaseUrl(context, auth);
|
|
121
|
+
if (!boundRoot || selectedRoot !== boundRoot) {
|
|
122
|
+
throw new Error('Authoritative Pod storage binding changed on cached context');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
117
125
|
this.logger.debug('Using cached db from context');
|
|
118
126
|
return cachedDb;
|
|
119
127
|
}
|
|
@@ -123,8 +131,12 @@ class PodChatKitStore {
|
|
|
123
131
|
// it never outlives the request or crosses credentials.
|
|
124
132
|
const inFlight = context._cachedDbPromise;
|
|
125
133
|
if (inFlight) {
|
|
134
|
+
if (context._cachedDbPromiseAuth !== this.podCredentialBinding(context)) {
|
|
135
|
+
throw new Error('Authoritative Pod credential binding changed while opening database');
|
|
136
|
+
}
|
|
126
137
|
return inFlight;
|
|
127
138
|
}
|
|
139
|
+
context._cachedDbPromiseAuth = this.podCredentialBinding(context);
|
|
128
140
|
const opening = this.openDb(context);
|
|
129
141
|
context._cachedDbPromise = opening;
|
|
130
142
|
try {
|
|
@@ -133,6 +145,7 @@ class PodChatKitStore {
|
|
|
133
145
|
finally {
|
|
134
146
|
if (context._cachedDbPromise === opening) {
|
|
135
147
|
delete context._cachedDbPromise;
|
|
148
|
+
delete context._cachedDbPromiseAuth;
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
}
|
|
@@ -142,6 +155,11 @@ class PodChatKitStore {
|
|
|
142
155
|
this.logger.warn('No valid solid auth in context, cannot access Pod');
|
|
143
156
|
throw new Error(CallerPodAccess_1.CALLER_POD_ACCESS_UNAVAILABLE);
|
|
144
157
|
}
|
|
158
|
+
const credentialBinding = this.podCredentialBinding(context);
|
|
159
|
+
// A missing authoritative binding is a storage failure, but it must not decide the diagnosis
|
|
160
|
+
// for a caller whose credential path is already unusable: resolve it here, then reject only
|
|
161
|
+
// after Pod access has reported why no usable credential exists.
|
|
162
|
+
const podBaseUrl = await this.resolveContextPodBaseUrl(context, auth);
|
|
145
163
|
// One credential path for every caller: the owner's own Pod key, exchanged for a
|
|
146
164
|
// token this process can prove, or the caller's reusable token. Unattended work carries a
|
|
147
165
|
// task-layer grant, which is forwarded so the Pod fetch is bound to that exact grant.
|
|
@@ -150,6 +168,7 @@ class PodChatKitStore {
|
|
|
150
168
|
try {
|
|
151
169
|
podFetch = await this.podAccess?.getPodFetch(auth.webId, {
|
|
152
170
|
auth,
|
|
171
|
+
podBaseUrl,
|
|
153
172
|
...(taskCredential ? { taskCredential } : {}),
|
|
154
173
|
});
|
|
155
174
|
}
|
|
@@ -162,11 +181,12 @@ class PodChatKitStore {
|
|
|
162
181
|
this.logger.warn(`No usable Pod credential for ${auth.webId}: ${reason}`);
|
|
163
182
|
throw new Error(reason);
|
|
164
183
|
}
|
|
165
|
-
const podBaseUrl = this.readExplicitPodBaseUrl(context)
|
|
166
|
-
?? this.normalizePodBaseUrl(await this.podBaseUrlResolver?.(auth.webId));
|
|
167
184
|
if (!podBaseUrl) {
|
|
168
185
|
throw new Error('Authoritative Pod storage binding unavailable');
|
|
169
186
|
}
|
|
187
|
+
if (credentialBinding !== this.podCredentialBinding(context)) {
|
|
188
|
+
throw new Error('Authoritative Pod credential binding changed while opening database');
|
|
189
|
+
}
|
|
170
190
|
const db = (0, drizzle_solid_1.drizzle)({ fetch: podFetch, info: { webId: auth.webId, isLoggedIn: true } }, { schema, podUrl: podBaseUrl });
|
|
171
191
|
this.logger.info(`Initializing tables for Pod: ${auth.webId}`);
|
|
172
192
|
try {
|
|
@@ -176,13 +196,42 @@ class PodChatKitStore {
|
|
|
176
196
|
catch (initError) {
|
|
177
197
|
this.logger.error(`Failed to init tables: ${initError}`);
|
|
178
198
|
}
|
|
199
|
+
if (credentialBinding !== this.podCredentialBinding(context)) {
|
|
200
|
+
throw new Error('Authoritative Pod credential binding changed while opening database');
|
|
201
|
+
}
|
|
179
202
|
context._cachedDb = db;
|
|
180
203
|
context._cachedFetch = podFetch;
|
|
181
204
|
context._cachedWebId = auth.webId;
|
|
182
|
-
context._cachedAuth =
|
|
205
|
+
context._cachedAuth = credentialBinding;
|
|
183
206
|
this.ensurePodBaseUrlCache(context, db);
|
|
184
207
|
return db;
|
|
185
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve the request's authoritative Pod root, or `undefined` when this deployment has no
|
|
211
|
+
* binding resolver to consult. Reporting a missing binding is left to the caller so the
|
|
212
|
+
* credential path can be classified first.
|
|
213
|
+
*/
|
|
214
|
+
async resolveContextPodBaseUrl(context, auth) {
|
|
215
|
+
const explicitRoot = this.readExplicitPodBaseUrl(context);
|
|
216
|
+
// Explicit internal contexts already carry a verified binding. A request selection or
|
|
217
|
+
// capability must still intersect that binding through the shared ownership resolver.
|
|
218
|
+
if (explicitRoot && !auth.requestedPodUrl && !auth.authorizedPodUrl)
|
|
219
|
+
return explicitRoot;
|
|
220
|
+
if (!this.podBaseUrlResolver)
|
|
221
|
+
return undefined;
|
|
222
|
+
const selectedRoot = this.normalizePodBaseUrl(await (0, PodBaseUrlResolver_1.resolveOwnerPodBaseUrl)(auth.webId, this.podBaseUrlResolver, auth));
|
|
223
|
+
if (explicitRoot && selectedRoot !== explicitRoot) {
|
|
224
|
+
throw new Error('Authoritative Pod storage binding conflicts with request');
|
|
225
|
+
}
|
|
226
|
+
return selectedRoot;
|
|
227
|
+
}
|
|
228
|
+
async requireContextPodBaseUrl(context, auth) {
|
|
229
|
+
const root = await this.resolveContextPodBaseUrl(context, auth);
|
|
230
|
+
if (!root) {
|
|
231
|
+
throw new Error('Authoritative Pod storage binding unavailable');
|
|
232
|
+
}
|
|
233
|
+
return root;
|
|
234
|
+
}
|
|
186
235
|
/** Stable identity of the credential binding behind a cached database, without secrets. */
|
|
187
236
|
podCredentialBinding(context) {
|
|
188
237
|
const auth = context.auth;
|
|
@@ -190,6 +239,8 @@ class PodChatKitStore {
|
|
|
190
239
|
return JSON.stringify({
|
|
191
240
|
webId: auth && (0, AuthContext_1.isSolidAuth)(auth) ? auth.webId : null,
|
|
192
241
|
clientId: auth && (0, AuthContext_1.isSolidAuth)(auth) ? auth.clientId ?? null : null,
|
|
242
|
+
requestedPodUrl: auth && (0, AuthContext_1.isSolidAuth)(auth) ? auth.requestedPodUrl ?? null : null,
|
|
243
|
+
authorizedPodUrl: auth && (0, AuthContext_1.isSolidAuth)(auth) ? auth.authorizedPodUrl ?? null : null,
|
|
193
244
|
taskCredential: task
|
|
194
245
|
? { ref: task.credentialRef ?? null, version: task.version ?? null, ownerGrant: task.ownerGrant ?? null }
|
|
195
246
|
: null,
|