@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
package/dist/api/ApiServer.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.ApiServer = void 0;
|
|
|
4
4
|
const node_http_1 = require("node:http");
|
|
5
5
|
const global_logger_factory_1 = require("global-logger-factory");
|
|
6
6
|
const NodeRuntimeHost_1 = require("../runtime/host/node/NodeRuntimeHost");
|
|
7
|
+
const PodAccessFailureResponse_1 = require("./handlers/PodAccessFailureResponse");
|
|
7
8
|
const task_model_diagnostics_1 = require("../util/task-model-diagnostics");
|
|
8
9
|
/**
|
|
9
10
|
* Standalone API Server
|
|
@@ -226,6 +227,8 @@ class ApiServer {
|
|
|
226
227
|
await route.handler(authRequest, response, params);
|
|
227
228
|
}
|
|
228
229
|
catch (error) {
|
|
230
|
+
if (!response.headersSent && (0, PodAccessFailureResponse_1.sendPodAccessFailure)(response, error))
|
|
231
|
+
return;
|
|
229
232
|
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
230
233
|
const causes = [];
|
|
231
234
|
let cause = error instanceof Error ? error.cause : undefined;
|
|
@@ -333,6 +336,7 @@ class ApiServer {
|
|
|
333
336
|
'Content-Type',
|
|
334
337
|
'Accept',
|
|
335
338
|
'DPoP',
|
|
339
|
+
'X-Xpod-Pod-Url',
|
|
336
340
|
'Origin',
|
|
337
341
|
'X-Requested-With',
|
|
338
342
|
'If-Match',
|
|
@@ -14,6 +14,31 @@ export interface StoredGatewayCredential extends GatewayCredentialCandidate {
|
|
|
14
14
|
secretPayload?: string;
|
|
15
15
|
version?: number;
|
|
16
16
|
runtimeCredential?: ProviderRuntimeCredential;
|
|
17
|
+
/**
|
|
18
|
+
* Stored session expiry. Lets the gateway ask the credential lifecycle to renew an expired
|
|
19
|
+
* OAuth session before it is used, without an extra read of the credential row.
|
|
20
|
+
*/
|
|
21
|
+
expiresAt?: Date;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Why the runtime asked for a renewal. `expired` is the proactive path (the stored session is
|
|
25
|
+
* already past its expiry); `authentication_failed` is the reactive path (upstream just rejected
|
|
26
|
+
* a credential the store still considered usable).
|
|
27
|
+
*/
|
|
28
|
+
export type GatewayCredentialRenewalReason = 'expired' | 'authentication_failed';
|
|
29
|
+
export interface GatewayCredentialRenewalRequest {
|
|
30
|
+
webId: string;
|
|
31
|
+
deployment: string;
|
|
32
|
+
provider: string;
|
|
33
|
+
credentialId: string;
|
|
34
|
+
credentialIri: string;
|
|
35
|
+
/**
|
|
36
|
+
* Version the caller observed. A different row version means a concurrent caller already
|
|
37
|
+
* renewed the credential, so the caller can retry with the freshly stored secret.
|
|
38
|
+
*/
|
|
39
|
+
observedVersion?: number;
|
|
40
|
+
reason: GatewayCredentialRenewalReason;
|
|
41
|
+
auth?: AuthContext;
|
|
17
42
|
}
|
|
18
43
|
export interface GatewayCredentialStore {
|
|
19
44
|
listCredentials(input: {
|
|
@@ -31,6 +56,13 @@ export interface GatewayCredentialStore {
|
|
|
31
56
|
encryptedSecret: EncryptedCredentialSecret;
|
|
32
57
|
auth?: AuthContext;
|
|
33
58
|
}): Promise<boolean>;
|
|
59
|
+
/**
|
|
60
|
+
* Shared credential lifecycle hook. Renews the stored OAuth session (re-reading the declared
|
|
61
|
+
* local session source or refreshing the stored refresh token) and reports whether the caller
|
|
62
|
+
* may retry with the credential that is now stored. Absent or false leaves the existing
|
|
63
|
+
* failure/failover behaviour untouched.
|
|
64
|
+
*/
|
|
65
|
+
renewCredential?(input: GatewayCredentialRenewalRequest): Promise<boolean>;
|
|
34
66
|
}
|
|
35
67
|
export interface GatewayCredentialHealthRecord {
|
|
36
68
|
webId: string;
|
|
@@ -122,6 +154,29 @@ export declare class AiGatewayService {
|
|
|
122
154
|
private executeWithCredentialFailover;
|
|
123
155
|
private recordUsage;
|
|
124
156
|
private findFailoverRoute;
|
|
157
|
+
/**
|
|
158
|
+
* Asks the shared credential lifecycle for a usable session whenever the stored one is already
|
|
159
|
+
* past its expiry. Non-OAuth credentials and stores without a lifecycle hook are left alone.
|
|
160
|
+
*/
|
|
161
|
+
private renewCredentialIfExpired;
|
|
162
|
+
/**
|
|
163
|
+
* Renews at most once per credential per request; returns whether a retry may proceed. A typed
|
|
164
|
+
* reauthorization requirement is rethrown so the caller surfaces it instead of issuing another
|
|
165
|
+
* provider request with a session that is already known to be invalid.
|
|
166
|
+
*/
|
|
167
|
+
private tryRenewCredential;
|
|
168
|
+
/**
|
|
169
|
+
* Re-reads the renewed row so the next attempt uses the new secret and its version. The reload
|
|
170
|
+
* must land on the same credential identity and the row must still be currently eligible: a
|
|
171
|
+
* concurrent disable, revoke or reauth between the lifecycle write and this read must not hand a
|
|
172
|
+
* revoked session back to the provider. Returns undefined when the credential is no longer usable.
|
|
173
|
+
*/
|
|
174
|
+
private reloadCredential;
|
|
175
|
+
/**
|
|
176
|
+
* Swaps the reloaded row into the route. Throws when the renewed row cannot be observed, because
|
|
177
|
+
* retrying with the stale secret would repeat the request the provider just rejected.
|
|
178
|
+
*/
|
|
179
|
+
private applyRenewedCredential;
|
|
125
180
|
private openApiKey;
|
|
126
181
|
private runtimeCredentialFor;
|
|
127
182
|
private offeringRuntimeCredential;
|
|
@@ -204,11 +204,25 @@ class AiGatewayService {
|
|
|
204
204
|
let route = input.route;
|
|
205
205
|
let firstClientEventEmitted = false;
|
|
206
206
|
const attempted = new Set();
|
|
207
|
+
// A single request may renew a given credential at most once, so a credential the provider
|
|
208
|
+
// keeps rejecting cannot turn into a refresh loop.
|
|
209
|
+
const renewalAttempted = new Set();
|
|
207
210
|
for (;;) {
|
|
211
|
+
// A request that is already cancelled must not start a renewal or another upstream attempt.
|
|
212
|
+
if (input.signal?.aborted)
|
|
213
|
+
throw abortError();
|
|
208
214
|
attempted.add(route.credential.id);
|
|
209
|
-
|
|
215
|
+
let credential = route.credential;
|
|
210
216
|
try {
|
|
211
217
|
let finalUsage;
|
|
218
|
+
if (await this.renewCredentialIfExpired(input.principal, credential, input.auth, renewalAttempted)) {
|
|
219
|
+
// The stored secret was replaced; reload the same row so the provider call below opens the
|
|
220
|
+
// renewed token instead of the stale route snapshot.
|
|
221
|
+
route = await this.applyRenewedCredential(input.principal, input.auth, route, credential);
|
|
222
|
+
credential = route.credential;
|
|
223
|
+
}
|
|
224
|
+
if (input.signal?.aborted)
|
|
225
|
+
throw abortError();
|
|
212
226
|
const apiKey = await this.openApiKey(input.principal, route, credential, input.auth);
|
|
213
227
|
const adapter = this.runtimes.get(route.provider.id);
|
|
214
228
|
const upstream = adapter.execute({
|
|
@@ -235,6 +249,23 @@ class AiGatewayService {
|
|
|
235
249
|
return;
|
|
236
250
|
}
|
|
237
251
|
catch (error) {
|
|
252
|
+
// A cancelled request must not renew, record health, or issue another provider request.
|
|
253
|
+
if (input.signal?.aborted)
|
|
254
|
+
throw abortError();
|
|
255
|
+
// Renew before recording health: a recoverable rejection must not be recorded as a
|
|
256
|
+
// credential failure that disables the session.
|
|
257
|
+
const renewed = !firstClientEventEmitted
|
|
258
|
+
&& isCredentialAuthenticationFailure(error)
|
|
259
|
+
&& await this.tryRenewCredential(input.principal, credential, input.auth, 'authentication_failed', renewalAttempted);
|
|
260
|
+
if (renewed) {
|
|
261
|
+
const fresh = await this.reloadCredential(input.principal, input.auth, credential);
|
|
262
|
+
// Only retry once the renewed row is observable; retrying with the stale secret would
|
|
263
|
+
// repeat the exact request the provider just rejected.
|
|
264
|
+
if (fresh) {
|
|
265
|
+
route = { ...route, credential: fresh };
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
238
269
|
await this.recordRouteFailure(input.principal.webId, route, error, input.auth);
|
|
239
270
|
if (!firstClientEventEmitted && this.router.canFailOver(route) && isCredentialFailoverError(error)) {
|
|
240
271
|
const nextRoute = await this.findFailoverRoute(input.principal.webId, input.auth, input.request, route, attempted);
|
|
@@ -293,6 +324,85 @@ class AiGatewayService {
|
|
|
293
324
|
throw error;
|
|
294
325
|
}
|
|
295
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Asks the shared credential lifecycle for a usable session whenever the stored one is already
|
|
329
|
+
* past its expiry. Non-OAuth credentials and stores without a lifecycle hook are left alone.
|
|
330
|
+
*/
|
|
331
|
+
async renewCredentialIfExpired(principal, credential, auth, renewalAttempted) {
|
|
332
|
+
const expiresAt = credential.expiresAt?.getTime();
|
|
333
|
+
if (expiresAt === undefined || expiresAt > this.now().getTime()) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
return this.tryRenewCredential(principal, credential, auth, 'expired', renewalAttempted);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Renews at most once per credential per request; returns whether a retry may proceed. A typed
|
|
340
|
+
* reauthorization requirement is rethrown so the caller surfaces it instead of issuing another
|
|
341
|
+
* provider request with a session that is already known to be invalid.
|
|
342
|
+
*/
|
|
343
|
+
async tryRenewCredential(principal, credential, auth, reason, renewalAttempted) {
|
|
344
|
+
const renew = this.credentials.renewCredential;
|
|
345
|
+
if (!renew || credential.authMode !== 'deviceCodeOAuth' || renewalAttempted.has(credential.id)) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
renewalAttempted.add(credential.id);
|
|
349
|
+
try {
|
|
350
|
+
return await renew.call(this.credentials, {
|
|
351
|
+
webId: principal.webId,
|
|
352
|
+
deployment: this.deployment,
|
|
353
|
+
provider: credential.provider,
|
|
354
|
+
credentialId: credential.id,
|
|
355
|
+
credentialIri: credential.credentialIri,
|
|
356
|
+
observedVersion: credential.version,
|
|
357
|
+
reason,
|
|
358
|
+
auth,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (isReauthRequiredError(error))
|
|
363
|
+
throw error;
|
|
364
|
+
// Log a typed safe code only: a raw error message could carry upstream token/response text.
|
|
365
|
+
this.logger.warn(`Credential renewal failed for provider ${credential.provider}: ${renewalFailureCode(error)}`);
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Re-reads the renewed row so the next attempt uses the new secret and its version. The reload
|
|
371
|
+
* must land on the same credential identity and the row must still be currently eligible: a
|
|
372
|
+
* concurrent disable, revoke or reauth between the lifecycle write and this read must not hand a
|
|
373
|
+
* revoked session back to the provider. Returns undefined when the credential is no longer usable.
|
|
374
|
+
*/
|
|
375
|
+
async reloadCredential(principal, auth, expected) {
|
|
376
|
+
try {
|
|
377
|
+
const credentials = await this.credentials.listCredentials({
|
|
378
|
+
webId: principal.webId,
|
|
379
|
+
deployment: this.deployment,
|
|
380
|
+
auth,
|
|
381
|
+
});
|
|
382
|
+
const candidate = credentials.find((row) => row.id === expected.id
|
|
383
|
+
&& row.credentialIri === expected.credentialIri
|
|
384
|
+
&& (0, ProviderRegistry_1.normalizeProviderId)(row.provider) === (0, ProviderRegistry_1.normalizeProviderId)(expected.provider));
|
|
385
|
+
return candidate && this.router.isCredentialEligible(candidate) ? candidate : undefined;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Swaps the reloaded row into the route. Throws when the renewed row cannot be observed, because
|
|
393
|
+
* retrying with the stale secret would repeat the request the provider just rejected.
|
|
394
|
+
*/
|
|
395
|
+
async applyRenewedCredential(principal, auth, route, credential) {
|
|
396
|
+
const fresh = await this.reloadCredential(principal, auth, credential);
|
|
397
|
+
if (!fresh) {
|
|
398
|
+
throw new errors_1.GatewayProtocolError('Renewed credential could not be reloaded', {
|
|
399
|
+
code: 'credential_unavailable',
|
|
400
|
+
status: 401,
|
|
401
|
+
details: { credentialId: credential.id },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return { ...route, credential: fresh };
|
|
405
|
+
}
|
|
296
406
|
async openApiKey(principal, route, credential, auth) {
|
|
297
407
|
if (credential.storageMode === 'plaintext-v1') {
|
|
298
408
|
const secret = (0, PlaintextCredentialPayload_1.decodePlaintextCredential)(credential);
|
|
@@ -413,11 +523,7 @@ class AiGatewayService {
|
|
|
413
523
|
};
|
|
414
524
|
}
|
|
415
525
|
async recordRouteFailure(webId, route, error, auth) {
|
|
416
|
-
const status =
|
|
417
|
-
? error.status
|
|
418
|
-
: error instanceof errors_1.GatewayProtocolError
|
|
419
|
-
? error.status
|
|
420
|
-
: undefined;
|
|
526
|
+
const status = providerFailureStatus(error);
|
|
421
527
|
const occurredAt = this.now();
|
|
422
528
|
const normalized = (0, errors_1.normalizeGatewayError)(error);
|
|
423
529
|
const failureCode = stringMetadata(normalized.error.details, 'classification') ?? (0, ProviderRuntimeAdapter_1.classifyProviderStatus)(status ?? 502);
|
|
@@ -593,6 +699,39 @@ function healthRecord(webId, deployment, route) {
|
|
|
593
699
|
expectedVersion: route.credential.version,
|
|
594
700
|
};
|
|
595
701
|
}
|
|
702
|
+
/** Upstream status carried by a provider failure, if any. */
|
|
703
|
+
function providerFailureStatus(error) {
|
|
704
|
+
if (typeof error?.status === 'number') {
|
|
705
|
+
return error.status;
|
|
706
|
+
}
|
|
707
|
+
return error instanceof errors_1.GatewayProtocolError ? error.status : undefined;
|
|
708
|
+
}
|
|
709
|
+
/** A cancelled request stops the failover loop without touching credential health. */
|
|
710
|
+
function abortError() {
|
|
711
|
+
const error = new Error('Request aborted');
|
|
712
|
+
error.name = 'AbortError';
|
|
713
|
+
return error;
|
|
714
|
+
}
|
|
715
|
+
/** A typed reauthorization requirement that must reach the caller instead of being swallowed. */
|
|
716
|
+
function isReauthRequiredError(error) {
|
|
717
|
+
return error instanceof errors_1.GatewayProtocolError
|
|
718
|
+
&& error.details?.reauthRequired === true;
|
|
719
|
+
}
|
|
720
|
+
/** Safe, non-secret renewal failure code for logs. */
|
|
721
|
+
function renewalFailureCode(error) {
|
|
722
|
+
return error instanceof errors_1.GatewayProtocolError ? error.code : 'renewal_failed';
|
|
723
|
+
}
|
|
724
|
+
/** True when the failure is an upstream authentication rejection that a renewal could recover. */
|
|
725
|
+
function isCredentialAuthenticationFailure(error) {
|
|
726
|
+
const status = providerFailureStatus(error);
|
|
727
|
+
if (status === undefined) {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
const classification = error instanceof errors_1.GatewayProtocolError
|
|
731
|
+
? stringMetadata(error.details, 'classification')
|
|
732
|
+
: undefined;
|
|
733
|
+
return (classification ?? (0, ProviderRuntimeAdapter_1.classifyProviderStatus)(status)) === 'authentication';
|
|
734
|
+
}
|
|
596
735
|
function isCredentialFailoverError(error) {
|
|
597
736
|
if (!(error instanceof errors_1.GatewayProtocolError)) {
|
|
598
737
|
const normalized = (0, errors_1.normalizeGatewayError)(error);
|
|
@@ -60,7 +60,8 @@ class AiConnectionsInvocationKeyIssuer {
|
|
|
60
60
|
}
|
|
61
61
|
async issueScoped(auth, scopes, model) {
|
|
62
62
|
const webId = (0, InvocationTokenCodec_1.requireCanonicalWebId)(auth.webId);
|
|
63
|
-
const
|
|
63
|
+
const podUrl = auth.authorizedPodUrl;
|
|
64
|
+
const cacheKey = `${webId}\n${podUrl ?? ''}\n${scopes.join(' ')}`;
|
|
64
65
|
const now = this.now();
|
|
65
66
|
const cached = this.cache.get(cacheKey);
|
|
66
67
|
if (cached && cached.expiresAt.getTime() - now.getTime() >= this.reuseSafetyMarginMs) {
|
|
@@ -68,7 +69,7 @@ class AiConnectionsInvocationKeyIssuer {
|
|
|
68
69
|
}
|
|
69
70
|
let pending = this.pending.get(cacheKey);
|
|
70
71
|
if (!pending) {
|
|
71
|
-
pending = Promise.resolve().then(() => this.createInvocation(cacheKey, webId, scopes, now));
|
|
72
|
+
pending = Promise.resolve().then(() => this.createInvocation(cacheKey, webId, scopes, now, podUrl));
|
|
72
73
|
this.pending.set(cacheKey, pending);
|
|
73
74
|
const clearPending = () => {
|
|
74
75
|
if (this.pending.get(cacheKey) === pending) {
|
|
@@ -80,7 +81,7 @@ class AiConnectionsInvocationKeyIssuer {
|
|
|
80
81
|
const issued = await pending;
|
|
81
82
|
return this.toInvocationConfig(issued, model);
|
|
82
83
|
}
|
|
83
|
-
createInvocation(cacheKey, webId, scopes, createdAt) {
|
|
84
|
+
createInvocation(cacheKey, webId, scopes, createdAt, podUrl) {
|
|
84
85
|
this.pruneCache(createdAt);
|
|
85
86
|
const expiresAt = new Date(createdAt.getTime() + this.ttlMs);
|
|
86
87
|
const plaintext = this.codec.encode({
|
|
@@ -88,6 +89,7 @@ class AiConnectionsInvocationKeyIssuer {
|
|
|
88
89
|
audience: this.audience,
|
|
89
90
|
issuer: this.issuer,
|
|
90
91
|
webId,
|
|
92
|
+
...(podUrl ? { podUrl } : {}),
|
|
91
93
|
scopes,
|
|
92
94
|
issuedAt: createdAt,
|
|
93
95
|
expiresAt,
|
|
@@ -1,53 +1,22 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
2
|
import type { Authenticator, AuthResult } from '../../auth/Authenticator';
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
revokedAt?: Date;
|
|
20
|
-
name?: string;
|
|
21
|
-
plaintext?: string;
|
|
22
|
-
/** CSS/OIDC client id of an issued credential; an external runtime identifier. */
|
|
23
|
-
clientCredentialId?: string;
|
|
24
|
-
status?: string;
|
|
25
|
-
appliedTo?: string;
|
|
26
|
-
appliedOn?: string;
|
|
27
|
-
appliedAt?: Date;
|
|
28
|
-
}
|
|
29
|
-
export declare const LEGACY_GATEWAY_KEY_AUTHENTICATION = "legacy-gateway-key-authentication";
|
|
30
|
-
export type GatewayAccessKeyRepositoryInternalAccessReason = typeof LEGACY_GATEWAY_KEY_AUTHENTICATION | 'gateway-key-verifier';
|
|
31
|
-
export interface GatewayAccessKeyRepositoryContext {
|
|
32
|
-
auth?: AuthContext;
|
|
33
|
-
/** Set when the gateway itself is verifying a key, rather than serving an owner request. */
|
|
34
|
-
gatewayKeyVerification?: {
|
|
35
|
-
reason: GatewayAccessKeyRepositoryInternalAccessReason;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
export interface GatewayAccessKeyRepository {
|
|
39
|
-
createKeyId?(owner: string, deployment: GatewayDeployment): string;
|
|
40
|
-
create(record: GatewayAccessKeyRecord, context?: GatewayAccessKeyRepositoryContext): Promise<GatewayAccessKeyRecord>;
|
|
41
|
-
findById(id: string, context?: GatewayAccessKeyRepositoryContext): Promise<GatewayAccessKeyRecord | undefined>;
|
|
42
|
-
listByOwner(owner: string, context?: GatewayAccessKeyRepositoryContext): Promise<GatewayAccessKeyRecord[]>;
|
|
43
|
-
setEnabled(id: string, enabled: boolean, changedAt: Date, context?: GatewayAccessKeyRepositoryContext): Promise<GatewayAccessKeyRecord | undefined>;
|
|
44
|
-
revoke(id: string, revokedAt: Date, context?: GatewayAccessKeyRepositoryContext): Promise<GatewayAccessKeyRecord | undefined>;
|
|
45
|
-
delete(id: string, context?: GatewayAccessKeyRepositoryContext): Promise<boolean>;
|
|
46
|
-
revealPlaintext(id: string, context?: GatewayAccessKeyRepositoryContext): Promise<string | undefined>;
|
|
47
|
-
touchLastUsed(id: string, lastUsedAt: Date, context?: GatewayAccessKeyRepositoryContext): Promise<void>;
|
|
48
|
-
}
|
|
3
|
+
import type { GatewayDeployment, InvocationTokenCodec } from './InvocationTokenCodec';
|
|
4
|
+
/**
|
|
5
|
+
* Verifies AI-Connections invocation tokens for the model gateway.
|
|
6
|
+
*
|
|
7
|
+
* This used to verify two kinds of bearer: an `xpod_gw_v1.*` Gateway API Key looked up in its
|
|
8
|
+
* owner's Pod, and an `xpod_inv_v1.*` invocation token. The Gateway API Key half is gone: no code
|
|
9
|
+
* issues those keys any more, and a key holder was already refused Pod access. Only the invocation
|
|
10
|
+
* half remains, unchanged.
|
|
11
|
+
*
|
|
12
|
+
* The name and the `viaGatewayApiKey` context flag it sets are kept deliberately. The flag is how
|
|
13
|
+
* `GatewayPrincipal.isGatewayApiKeyPrincipal`, `AiGatewayService.isGatewayKeySolidPrincipal`,
|
|
14
|
+
* `CallerPodAccess`, `AiConfigHandler` and `AiConnectionsInvocationKeyIssuer` recognise an
|
|
15
|
+
* invocation principal, so renaming it here would ripple through the very path this file must keep
|
|
16
|
+
* working. The `Invalid gateway API key` error text is likewise the wire behaviour invocation
|
|
17
|
+
* failures already produce.
|
|
18
|
+
*/
|
|
49
19
|
export interface GatewayApiKeyAuthenticatorOptions {
|
|
50
|
-
repository?: GatewayAccessKeyRepository;
|
|
51
20
|
deployment: GatewayDeployment;
|
|
52
21
|
requiredScopes?: string[];
|
|
53
22
|
invocationTokenCodec?: InvocationTokenCodec;
|
|
@@ -58,7 +27,6 @@ export interface GatewayApiKeyAuthenticatorOptions {
|
|
|
58
27
|
}
|
|
59
28
|
export declare const DEFAULT_GATEWAY_API_KEY_SCOPES: readonly ["models:read", "inference:write"];
|
|
60
29
|
export declare class GatewayApiKeyAuthenticator implements Authenticator {
|
|
61
|
-
private readonly repository?;
|
|
62
30
|
private readonly deployment;
|
|
63
31
|
private readonly requiredScopes;
|
|
64
32
|
private readonly invocationTokenCodec?;
|
|
@@ -66,7 +34,6 @@ export declare class GatewayApiKeyAuthenticator implements Authenticator {
|
|
|
66
34
|
private readonly invocationTokenIssuer?;
|
|
67
35
|
private readonly now;
|
|
68
36
|
private readonly maxClockSkewMs;
|
|
69
|
-
private readonly dummyHash;
|
|
70
37
|
constructor(options: GatewayApiKeyAuthenticatorOptions);
|
|
71
38
|
canAuthenticate(request: IncomingMessage): boolean;
|
|
72
39
|
authenticate(request: IncomingMessage): Promise<AuthResult>;
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GatewayApiKeyAuthenticator = exports.DEFAULT_GATEWAY_API_KEY_SCOPES =
|
|
3
|
+
exports.GatewayApiKeyAuthenticator = exports.DEFAULT_GATEWAY_API_KEY_SCOPES = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
const GatewayApiKey_1 = require("./GatewayApiKey");
|
|
6
5
|
const InvocationTokenCodec_1 = require("./InvocationTokenCodec");
|
|
7
|
-
exports.LEGACY_GATEWAY_KEY_AUTHENTICATION = 'legacy-gateway-key-authentication';
|
|
8
6
|
const INVALID_GATEWAY_API_KEY = 'Invalid gateway API key';
|
|
9
7
|
exports.DEFAULT_GATEWAY_API_KEY_SCOPES = ['models:read', 'inference:write'];
|
|
8
|
+
const INVOCATION_TOKEN_PREFIX = 'xpod_inv_v1.';
|
|
10
9
|
class GatewayApiKeyAuthenticator {
|
|
11
10
|
constructor(options) {
|
|
12
|
-
this.repository = options.repository;
|
|
13
11
|
this.deployment = options.deployment;
|
|
14
12
|
this.requiredScopes = options.requiredScopes ?? [...exports.DEFAULT_GATEWAY_API_KEY_SCOPES];
|
|
15
13
|
this.invocationTokenCodec = options.invocationTokenCodec;
|
|
@@ -24,68 +22,16 @@ class GatewayApiKeyAuthenticator {
|
|
|
24
22
|
if (!Number.isSafeInteger(this.maxClockSkewMs) || this.maxClockSkewMs < 0 || this.maxClockSkewMs > 30_000) {
|
|
25
23
|
throw new Error('Gateway invocation token clock skew must be between 0 and 30000 milliseconds');
|
|
26
24
|
}
|
|
27
|
-
this.dummyHash = (0, GatewayApiKey_1.hashGatewayApiKeySecret)('xpod-gateway-missing-key-dummy-secret');
|
|
28
25
|
}
|
|
29
26
|
canAuthenticate(request) {
|
|
30
|
-
|
|
31
|
-
return Boolean(bearer
|
|
32
|
-
&& (bearer.startsWith('xpod_inv_v1.') || (0, GatewayApiKey_1.parseGatewayApiKey)(bearer)));
|
|
27
|
+
return Boolean(this.readBearer(request)?.startsWith(INVOCATION_TOKEN_PREFIX));
|
|
33
28
|
}
|
|
34
29
|
async authenticate(request) {
|
|
35
30
|
const bearer = this.readBearer(request);
|
|
36
|
-
if (bearer?.startsWith(
|
|
37
|
-
return this.authenticateInvocationToken(bearer);
|
|
38
|
-
}
|
|
39
|
-
const parsed = bearer ? (0, GatewayApiKey_1.parseGatewayApiKey)(bearer) : undefined;
|
|
40
|
-
if (!parsed) {
|
|
41
|
-
return { success: false, error: INVALID_GATEWAY_API_KEY };
|
|
42
|
-
}
|
|
43
|
-
if (!this.repository) {
|
|
44
|
-
return infrastructureError(new Error('Gateway API key repository is not configured'));
|
|
45
|
-
}
|
|
46
|
-
const repositoryContext = {
|
|
47
|
-
gatewayKeyVerification: { reason: 'gateway-key-verifier' },
|
|
48
|
-
};
|
|
49
|
-
let record;
|
|
50
|
-
try {
|
|
51
|
-
record = await this.repository.findById(parsed.keyId, repositoryContext);
|
|
52
|
-
}
|
|
53
|
-
catch (cause) {
|
|
54
|
-
return infrastructureError(cause);
|
|
55
|
-
}
|
|
56
|
-
if (!record || record.kind === 'client-credentials') {
|
|
57
|
-
await (0, GatewayApiKey_1.verifyGatewayApiKeySecret)(parsed.secret, await this.dummyHash);
|
|
58
|
-
return invalidGatewayApiKey();
|
|
59
|
-
}
|
|
60
|
-
const secretMatches = await (0, GatewayApiKey_1.verifyGatewayApiKeySecret)(parsed.secret, record.secretHash);
|
|
61
|
-
if (!secretMatches
|
|
62
|
-
|| parsed.deployment !== this.deployment
|
|
63
|
-
|| record.deployment !== this.deployment
|
|
64
|
-
|| record.revokedAt
|
|
65
|
-
|| record.disabledAt
|
|
66
|
-
|| isExpired(record, this.now())
|
|
67
|
-
|| !hasRequiredScopes(record.scopes, this.requiredScopes)) {
|
|
31
|
+
if (!bearer?.startsWith(INVOCATION_TOKEN_PREFIX)) {
|
|
68
32
|
return invalidGatewayApiKey();
|
|
69
33
|
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await this.repository.touchLastUsed(record.id, lastUsedAt, repositoryContext);
|
|
73
|
-
}
|
|
74
|
-
catch (cause) {
|
|
75
|
-
return infrastructureError(cause);
|
|
76
|
-
}
|
|
77
|
-
const context = {
|
|
78
|
-
type: 'solid',
|
|
79
|
-
webId: record.owner,
|
|
80
|
-
accountId: record.owner,
|
|
81
|
-
viaGatewayApiKey: true,
|
|
82
|
-
gatewayRuntimeAccess: true,
|
|
83
|
-
gatewayKeyId: record.id,
|
|
84
|
-
gatewayKeyFingerprint: fingerprintGatewayBearer(bearer),
|
|
85
|
-
scopes: record.scopes,
|
|
86
|
-
tokenType: 'Bearer',
|
|
87
|
-
};
|
|
88
|
-
return { success: true, context };
|
|
34
|
+
return this.authenticateInvocationToken(bearer);
|
|
89
35
|
}
|
|
90
36
|
authenticateInvocationToken(token) {
|
|
91
37
|
const claims = this.invocationTokenCodec?.decode(token);
|
|
@@ -95,6 +41,7 @@ class GatewayApiKeyAuthenticator {
|
|
|
95
41
|
const context = {
|
|
96
42
|
type: 'solid',
|
|
97
43
|
webId: claims.webId,
|
|
44
|
+
...(claims.podUrl ? { authorizedPodUrl: claims.podUrl } : {}),
|
|
98
45
|
accountId: claims.webId,
|
|
99
46
|
viaGatewayApiKey: true,
|
|
100
47
|
internalInvocation: true,
|
|
@@ -134,18 +81,6 @@ function invalidGatewayApiKey() {
|
|
|
134
81
|
statusCode: 401,
|
|
135
82
|
};
|
|
136
83
|
}
|
|
137
|
-
function infrastructureError(cause) {
|
|
138
|
-
return {
|
|
139
|
-
success: false,
|
|
140
|
-
error: 'Gateway API key authentication unavailable',
|
|
141
|
-
category: 'service_unavailable',
|
|
142
|
-
statusCode: 503,
|
|
143
|
-
cause,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function isExpired(record, now) {
|
|
147
|
-
return Boolean(record.expiresAt && record.expiresAt.getTime() <= now.getTime());
|
|
148
|
-
}
|
|
149
84
|
function hasRequiredScopes(scopes, requiredScopes) {
|
|
150
85
|
return requiredScopes.every((scope) => scopes.includes(scope));
|
|
151
86
|
}
|
|
@@ -5,6 +5,8 @@ export interface InvocationTokenClaims {
|
|
|
5
5
|
audience: string;
|
|
6
6
|
issuer: string;
|
|
7
7
|
webId: string;
|
|
8
|
+
/** Signed canonical storage binding; absent only on legacy unbound invocations. */
|
|
9
|
+
podUrl?: string;
|
|
8
10
|
scopes: string[];
|
|
9
11
|
issuedAt: Date;
|
|
10
12
|
expiresAt: Date;
|
|
@@ -20,6 +22,8 @@ export interface InvocationTokenInput {
|
|
|
20
22
|
audience: string;
|
|
21
23
|
issuer: string;
|
|
22
24
|
webId: string;
|
|
25
|
+
/** Signed canonical storage binding; absent only on legacy unbound invocations. */
|
|
26
|
+
podUrl?: string;
|
|
23
27
|
scopes: string[];
|
|
24
28
|
issuedAt: Date;
|
|
25
29
|
expiresAt: Date;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AesInvocationTokenCodec = void 0;
|
|
4
4
|
exports.requireCanonicalOrigin = requireCanonicalOrigin;
|
|
5
5
|
exports.requireCanonicalWebId = requireCanonicalWebId;
|
|
6
|
+
const PodBaseUrlResolver_1 = require("../pod/PodBaseUrlResolver");
|
|
6
7
|
const node_crypto_1 = require("node:crypto");
|
|
7
8
|
const PREFIX = 'xpod_inv_v1';
|
|
8
9
|
const AAD_CONTEXT = 'xpod:gateway:internal-invocation:v1';
|
|
@@ -42,6 +43,7 @@ class AesInvocationTokenCodec {
|
|
|
42
43
|
aud: audience,
|
|
43
44
|
iss: issuer,
|
|
44
45
|
webId,
|
|
46
|
+
...(input.podUrl !== undefined ? { podUrl: (0, PodBaseUrlResolver_1.normalizeVerifiedPodRoot)(input.podUrl) } : {}),
|
|
45
47
|
scopes,
|
|
46
48
|
iat: issuedAt,
|
|
47
49
|
exp: expiresAt,
|
|
@@ -97,7 +99,9 @@ class AesInvocationTokenCodec {
|
|
|
97
99
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
98
100
|
const parsed = JSON.parse(plaintext.toString('utf8'));
|
|
99
101
|
const keys = Object.keys(parsed).sort();
|
|
100
|
-
if (keys.join(',') !==
|
|
102
|
+
if (keys.join(',') !== (parsed.podUrl === undefined
|
|
103
|
+
? 'aud,deployment,exp,iat,iss,jti,kid,scopes,v,webId'
|
|
104
|
+
: 'aud,deployment,exp,iat,iss,jti,kid,podUrl,scopes,v,webId')
|
|
101
105
|
||
|
|
102
106
|
parsed.v !== 1
|
|
103
107
|
|| parsed.kid !== kid
|
|
@@ -105,6 +109,7 @@ class AesInvocationTokenCodec {
|
|
|
105
109
|
|| typeof parsed.aud !== 'string'
|
|
106
110
|
|| typeof parsed.iss !== 'string'
|
|
107
111
|
|| typeof parsed.webId !== 'string'
|
|
112
|
+
|| (parsed.podUrl !== undefined && typeof parsed.podUrl !== 'string')
|
|
108
113
|
|| !Array.isArray(parsed.scopes)
|
|
109
114
|
|| typeof parsed.iat !== 'number'
|
|
110
115
|
|| typeof parsed.exp !== 'number'
|
|
@@ -115,6 +120,7 @@ class AesInvocationTokenCodec {
|
|
|
115
120
|
const audience = requireCanonicalOrigin(parsed.aud, 'audience');
|
|
116
121
|
const issuer = requireCanonicalOrigin(parsed.iss, 'issuer');
|
|
117
122
|
const webId = requireCanonicalWebId(parsed.webId);
|
|
123
|
+
const podUrl = parsed.podUrl === undefined ? undefined : (0, PodBaseUrlResolver_1.normalizeVerifiedPodRoot)(parsed.podUrl);
|
|
118
124
|
const scopes = requireScopes(parsed.scopes);
|
|
119
125
|
const issuedAt = requireEpoch(parsed.iat);
|
|
120
126
|
const expiresAt = requireEpoch(parsed.exp);
|
|
@@ -128,6 +134,7 @@ class AesInvocationTokenCodec {
|
|
|
128
134
|
audience,
|
|
129
135
|
issuer,
|
|
130
136
|
webId,
|
|
137
|
+
...(podUrl ? { podUrl } : {}),
|
|
131
138
|
scopes,
|
|
132
139
|
issuedAt: new Date(issuedAt),
|
|
133
140
|
expiresAt: new Date(expiresAt),
|
|
@@ -22,6 +22,8 @@ export interface LocalSessionImportAdapter {
|
|
|
22
22
|
export interface FileSessionImportAdapterOptions {
|
|
23
23
|
profile: SessionImportProfile;
|
|
24
24
|
homeDir?: string;
|
|
25
|
+
/** Environment snapshot used to resolve declared overrides; defaults to `process.env`. */
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
25
27
|
readFile?: (filePath: string, encoding: BufferEncoding) => Promise<string>;
|
|
26
28
|
maxBytes?: number;
|
|
27
29
|
}
|
|
@@ -30,6 +32,7 @@ export declare class FileSessionImportAdapter implements LocalSessionImportAdapt
|
|
|
30
32
|
readonly offeringId: string;
|
|
31
33
|
private readonly profile;
|
|
32
34
|
private readonly homeDir;
|
|
35
|
+
private readonly env;
|
|
33
36
|
private readonly readFile;
|
|
34
37
|
private readonly maxBytes;
|
|
35
38
|
constructor(options: FileSessionImportAdapterOptions);
|