@undefineds.co/xpod 0.4.28 → 0.4.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/api/ApiServer.js +4 -0
  2. package/dist/api/ai-gateway/AiGatewayService.d.ts +55 -0
  3. package/dist/api/ai-gateway/AiGatewayService.js +145 -6
  4. package/dist/api/ai-gateway/auth/AiConnectionsInvocationKeyIssuer.js +5 -3
  5. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.d.ts +16 -49
  6. package/dist/api/ai-gateway/auth/GatewayApiKeyAuthenticator.js +6 -71
  7. package/dist/api/ai-gateway/auth/InvocationTokenAuthenticator.js +1 -0
  8. package/dist/api/ai-gateway/auth/InvocationTokenCodec.d.ts +4 -0
  9. package/dist/api/ai-gateway/auth/InvocationTokenCodec.js +8 -1
  10. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.d.ts +3 -0
  11. package/dist/api/ai-gateway/connect/FileSessionImportAdapter.js +23 -3
  12. package/dist/api/ai-gateway/connect/SessionImportProfiles.d.ts +9 -1
  13. package/dist/api/ai-gateway/connect/SessionImportProfiles.js +4 -3
  14. package/dist/api/ai-gateway/connect/index.d.ts +40 -0
  15. package/dist/api/ai-gateway/connect/index.js +194 -4
  16. package/dist/api/ai-gateway/models/PodModelSelectionRepository.js +1 -1
  17. package/dist/api/ai-gateway/pod/OwnerPodAccess.js +5 -0
  18. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.d.ts +11 -2
  19. package/dist/api/ai-gateway/pod/PodBaseUrlResolver.js +72 -10
  20. package/dist/api/ai-gateway/quota/ProviderQuotaAdapter.js +1 -1
  21. package/dist/api/ai-gateway/routing/ModelRouter.d.ts +7 -0
  22. package/dist/api/ai-gateway/routing/ModelRouter.js +13 -10
  23. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.d.ts +1 -5
  24. package/dist/api/ai-gateway/service-access/AiConnectionsServiceAccess.js +8 -63
  25. package/dist/api/auth/AuthContext.d.ts +4 -0
  26. package/dist/api/auth/ClientCredentialsAuthenticator.js +11 -5
  27. package/dist/api/auth/SolidSessionFactory.d.ts +50 -4
  28. package/dist/api/auth/SolidSessionFactory.js +114 -4
  29. package/dist/api/chatkit/pod-store.d.ts +8 -1
  30. package/dist/api/chatkit/pod-store.js +54 -3
  31. package/dist/api/container/common.js +34 -60
  32. package/dist/api/container/index.js +0 -3
  33. package/dist/api/container/routes.js +1 -8
  34. package/dist/api/container/types.d.ts +2 -9
  35. package/dist/api/container/types.js +0 -5
  36. package/dist/api/handlers/AiGatewayManagementHandler.d.ts +4 -12
  37. package/dist/api/handlers/AiGatewayManagementHandler.js +18 -258
  38. package/dist/api/middleware/AuthMiddleware.js +29 -1
  39. package/dist/api/runs/PiAgentRuntimeDriver.js +6 -1
  40. package/dist/cli/index.js +2 -1
  41. package/dist/identity/oidc/PodOwnershipResolver.js +16 -6
  42. package/dist/runtime/ingress-port.d.ts +9 -2
  43. package/dist/runtime/ingress-port.js +7 -2
  44. package/dist/runtime/local-secret-file.d.ts +1 -1
  45. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.d.ts +3 -1
  46. package/dist/runtime/runner/node/CommunitySolidServerCssRunner.js +31 -1
  47. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.d.ts +3 -1
  48. package/node_modules/@undefineds.co/ai-connections/dist/AiConnectionsPanel.js +96 -67
  49. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.d.ts +2 -1
  50. package/node_modules/@undefineds.co/ai-connections/dist/AiCredentialPoolSection.js +2 -2
  51. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.d.ts +1 -2
  52. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeyRow.js +2 -2
  53. package/node_modules/@undefineds.co/ai-connections/dist/AiGatewayKeysSection.js +81 -23
  54. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.d.ts +2 -1
  55. package/node_modules/@undefineds.co/ai-connections/dist/AiOfferingDetails.js +2 -2
  56. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.d.ts +2 -1
  57. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderCard.js +2 -2
  58. package/node_modules/@undefineds.co/ai-connections/dist/AiProviderHeader.js +1 -1
  59. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.d.ts +1 -0
  60. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/fingerprint.js +7 -0
  61. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.d.ts +1 -2
  62. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/normalize.js +0 -33
  63. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.d.ts +1 -1
  64. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/request.js +68 -22
  65. package/node_modules/@undefineds.co/ai-connections/dist/contract/client/types.d.ts +37 -10
  66. package/node_modules/@undefineds.co/ai-connections/dist/controller.js +5 -54
  67. package/node_modules/@undefineds.co/ai-connections/dist/error-wording.js +7 -2
  68. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.cjs +405 -0
  69. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.d.ts +14 -0
  70. package/node_modules/@undefineds.co/ai-connections/dist/service-access-resources.js +36 -0
  71. package/node_modules/@undefineds.co/ai-connections/dist/service-access.js +5 -26
  72. package/node_modules/@undefineds.co/ai-connections/package.json +10 -2
  73. package/node_modules/@undefineds.co/extension-sdk/dist/solid-permissions.js +131 -13
  74. package/node_modules/@undefineds.co/extension-sdk/dist/web.d.ts +7 -1
  75. package/node_modules/@undefineds.co/extension-sdk/package.json +1 -1
  76. package/node_modules/@undefineds.co/pod-collections/dist/index.cjs +38 -5
  77. package/node_modules/@undefineds.co/pod-collections/dist/mapping.d.ts +8 -0
  78. package/node_modules/@undefineds.co/pod-collections/dist/mapping.js +34 -0
  79. package/node_modules/@undefineds.co/pod-collections/dist/mutations.d.ts +2 -1
  80. package/node_modules/@undefineds.co/pod-collections/dist/mutations.js +6 -4
  81. package/node_modules/@undefineds.co/pod-collections/dist/sync.js +4 -3
  82. package/node_modules/@undefineds.co/pod-collections/package.json +1 -1
  83. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +1 -1
  84. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +2 -2
  85. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +2 -2
  86. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +3 -3
  87. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +5 -5
  88. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +5 -5
  89. package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +5 -5
  90. package/node_modules/@undefineds.co/shared-ui/dist/theme.css +32 -5
  91. package/node_modules/@undefineds.co/shared-ui/package.json +1 -1
  92. package/package.json +2 -2
  93. package/static/app/assets/main.css +1 -1
  94. package/static/app/assets/main.js +18 -18
  95. package/static/dashboard/assets/{AccountAuthBoundary-Dv52SZc6.js → AccountAuthBoundary-CuqXFEW4.js} +1 -1
  96. package/static/dashboard/assets/{DesktopOidcCallback-DaT_Rumf.js → DesktopOidcCallback-7i-yOvPW.js} +1 -1
  97. package/static/dashboard/assets/DevicePages-D1m_BNtX.js +2 -0
  98. package/static/dashboard/assets/ModelsPage-0qWGa5Y6.js +12 -0
  99. package/static/dashboard/assets/{PaneListHeader-DtvnRmFq.js → PaneListHeader-uWdPgJ0E.js} +1 -1
  100. package/static/dashboard/assets/{PodDeletionAuthorizationPanel-BJk_8rJq.js → PodDeletionAuthorizationPanel-C22bhZeD.js} +3 -3
  101. package/static/dashboard/assets/{PodPage-C4gzEspV.js → PodPage-Dyo3yCb-.js} +1 -1
  102. package/static/dashboard/assets/{SystemSettingsPage-HaNrP1H1.js → SystemSettingsPage-CeX-oG_a.js} +1 -1
  103. package/static/dashboard/assets/{TasksPage-BdO5K2L7.js → TasksPage-B0EoyRYT.js} +1 -1
  104. package/static/dashboard/assets/{WebDesktopEntry-Cj9_ZfJU.js → WebDesktopEntry-DT44RrTa.js} +1 -1
  105. package/static/dashboard/assets/XpodAiConnectionsPodStore-EXCqZPmb.js +3 -0
  106. package/static/dashboard/assets/XpodShellApp-BEzi4f1j.css +1 -0
  107. package/static/dashboard/assets/XpodShellApp-DAFsxamX.js +4 -0
  108. package/static/dashboard/assets/{account-session-DlcKNG7k.js → account-session-rRAOF7nF.js} +1 -1
  109. package/static/dashboard/assets/{button-wslF55uZ.js → button-WVnQUNG-.js} +1 -1
  110. package/static/dashboard/assets/{collection-runtime-uxxgEu13.js → collection-runtime-BAJQ8Qub.js} +1 -1
  111. package/static/dashboard/assets/{createLucideIcon-BJQl82Qk.js → createLucideIcon-BWelSi3L.js} +1 -1
  112. package/static/dashboard/assets/dashboard-hSOBxPcP.css +1 -0
  113. package/static/dashboard/assets/{dashboard-Or0gnkou.js → dashboard-vmne1ZE0.js} +2 -2
  114. package/static/dashboard/assets/{index-BqFeOML9.js → index-HDQQ8asz.js} +1 -1
  115. package/static/dashboard/assets/{index-DgKn1twH.js → index-gtv-MAHl.js} +1 -1
  116. package/static/dashboard/assets/{label-CS6Sx6Zb.js → label-tKuCTJm2.js} +1 -1
  117. package/static/dashboard/assets/{list-UyhlCFKk.js → list-CKq3KHke.js} +1 -1
  118. package/static/dashboard/assets/{nav-item-style-CW1Nnorq.js → nav-item-style-B2gI7c52.js} +1 -1
  119. package/static/dashboard/assets/pod-collections-host-BqOAkacM.js +8 -0
  120. package/static/dashboard/assets/{safe-continuation-ClWAhcma.js → safe-continuation-BsKjDpAA.js} +1 -1
  121. package/static/dashboard/assets/{section-header-CEIEcco-.js → section-header-DM0JU-wk.js} +1 -1
  122. package/static/dashboard/assets/{setting-row-CaYv3dfF.js → setting-row-XiH7UjWy.js} +1 -1
  123. package/static/dashboard/assets/{xpod-remembered-login-BUF9fin1.js → xpod-remembered-login-nv1g1sfC.js} +4 -4
  124. package/static/dashboard/dashboard.html +2 -2
  125. package/static/settings/assets/{AccountAuthBoundary-Chqd38oU.js → AccountAuthBoundary-DAUR0omy.js} +1 -1
  126. package/static/settings/assets/{DesktopOidcCallback-VUrOSM_q.js → DesktopOidcCallback-BNZAZMWY.js} +1 -1
  127. package/static/settings/assets/DevicePages-BO9SrXRp.js +2 -0
  128. package/static/settings/assets/ModelsPage-DREEXdf3.js +12 -0
  129. package/static/settings/assets/{PaneListHeader-B5RkRqr4.js → PaneListHeader-CeUH0JjQ.js} +1 -1
  130. package/static/settings/assets/{PodDeletionAuthorizationPanel-DPM-5Izt.js → PodDeletionAuthorizationPanel-Ch4--l4b.js} +3 -3
  131. package/static/settings/assets/{PodPage-CxND6wwc.js → PodPage-CqJBj3GJ.js} +1 -1
  132. package/static/settings/assets/{SystemSettingsPage-C8EdMkW7.js → SystemSettingsPage-2LSjpqUa.js} +1 -1
  133. package/static/settings/assets/{TasksPage-DuqptEm8.js → TasksPage-DA5IRxlP.js} +1 -1
  134. package/static/settings/assets/{WebDesktopEntry-BRcyktkv.js → WebDesktopEntry-BVelF1q0.js} +1 -1
  135. package/static/settings/assets/XpodAiConnectionsPodStore-O5-2qBsx.js +3 -0
  136. package/static/settings/assets/XpodShellApp-BEzi4f1j.css +1 -0
  137. package/static/settings/assets/XpodShellApp-D1b3kqD0.js +4 -0
  138. package/static/settings/assets/XpodThemeProvider-hSOBxPcP.css +1 -0
  139. package/static/settings/assets/{XpodThemeProvider-DK3pIdRG.js → XpodThemeProvider-rUdAw66p.js} +2 -2
  140. package/static/settings/assets/{account-session-DJOZ03xr.js → account-session-BD1q8D_o.js} +1 -1
  141. package/static/settings/assets/auth-callback-PWS7okI8.js +1 -0
  142. package/static/settings/assets/{button-D3yW-2o1.js → button-ZJYFB218.js} +1 -1
  143. package/static/settings/assets/{collection-runtime-DfGo25mh.js → collection-runtime-C2FHCCXJ.js} +1 -1
  144. package/static/settings/assets/{createLucideIcon-CacwLsmB.js → createLucideIcon-B2wXvYTD.js} +1 -1
  145. package/static/settings/assets/{index-r5KRYsKC.js → index-JFAkaKZH.js} +1 -1
  146. package/static/settings/assets/{label-MJjUmvzG.js → label-BLrTAmGy.js} +1 -1
  147. package/static/settings/assets/{list-Ce-5k1MX.js → list-Cm96vvJO.js} +1 -1
  148. package/static/settings/assets/{nav-item-style-ZUbL8KOZ.js → nav-item-style-C2upI7sr.js} +1 -1
  149. package/static/settings/assets/pod-collections-host-BqOAkacM.js +8 -0
  150. package/static/settings/assets/{safe-continuation-vlBZFSlU.js → safe-continuation-CYaRj3rj.js} +1 -1
  151. package/static/settings/assets/{section-header-DGJdEvQw.js → section-header-BviL5H8E.js} +1 -1
  152. package/static/settings/assets/{setting-row-C7tuFiho.js → setting-row-BYikj0YO.js} +1 -1
  153. package/static/settings/assets/{settings-CyQPih1O.js → settings-CpoDG_Qr.js} +1 -1
  154. package/static/settings/assets/{xpod-remembered-login-D-TzzT5K.js → xpod-remembered-login-BJ4QUSXy.js} +4 -4
  155. package/static/settings/auth-callback.html +3 -3
  156. package/static/settings/settings.html +3 -3
  157. package/dist/api/ai-gateway/auth/GatewayApiKey.d.ts +0 -28
  158. package/dist/api/ai-gateway/auth/GatewayApiKey.js +0 -162
  159. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.d.ts +0 -26
  160. package/dist/api/ai-gateway/auth/GatewayKeyLocatorCodec.js +0 -97
  161. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.d.ts +0 -67
  162. package/dist/api/ai-gateway/auth/PodGatewayAccessKeyRepository.js +0 -340
  163. package/dist/runtime/gateway-locator-secret.d.ts +0 -5
  164. package/dist/runtime/gateway-locator-secret.js +0 -30
  165. package/static/dashboard/assets/DevicePages-HjclpIVm.js +0 -2
  166. package/static/dashboard/assets/ModelsPage-Be2TPd-x.js +0 -11
  167. package/static/dashboard/assets/XpodAiConnectionsPodStore-B7gmXBmC.js +0 -3
  168. package/static/dashboard/assets/XpodShellApp-Bwom2xL3.css +0 -1
  169. package/static/dashboard/assets/XpodShellApp-D_KY4nCW.js +0 -4
  170. package/static/dashboard/assets/dashboard-CbJ-JkrI.css +0 -1
  171. package/static/dashboard/assets/pod-collections-host-DjK2xf-b.js +0 -8
  172. package/static/settings/assets/DevicePages-C6qwvWwu.js +0 -2
  173. package/static/settings/assets/ModelsPage-Ry_dObRQ.js +0 -11
  174. package/static/settings/assets/XpodAiConnectionsPodStore-CyA_BJVC.js +0 -3
  175. package/static/settings/assets/XpodShellApp-Bwom2xL3.css +0 -1
  176. package/static/settings/assets/XpodShellApp-CsivHJl0.js +0 -4
  177. package/static/settings/assets/XpodThemeProvider-CbJ-JkrI.css +0 -1
  178. package/static/settings/assets/auth-callback-GIBYEz_7.js +0 -1
  179. package/static/settings/assets/pod-collections-host-DjK2xf-b.js +0 -8
@@ -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
- const credential = route.credential;
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 = typeof error?.status === 'number'
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 cacheKey = `${webId}\n${scopes.join(' ')}`;
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 { AuthContext } from '../../auth/AuthContext';
4
- import { type GatewayDeployment } from './GatewayApiKey';
5
- import type { InvocationTokenCodec } from './InvocationTokenCodec';
6
- export interface GatewayAccessKeyRecord {
7
- id: string;
8
- /** CSS credentials are saved configuration, never legacy Gateway authentication records. */
9
- kind?: 'client-credentials';
10
- credentialResource?: string;
11
- owner: string;
12
- secretHash: string;
13
- deployment: GatewayDeployment;
14
- scopes: string[];
15
- createdAt: Date;
16
- expiresAt?: Date;
17
- lastUsedAt?: Date;
18
- disabledAt?: Date;
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 = exports.LEGACY_GATEWAY_KEY_AUTHENTICATION = void 0;
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
- const bearer = this.readBearer(request);
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('xpod_inv_v1.')) {
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
- const lastUsedAt = this.now();
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
  }
@@ -34,6 +34,7 @@ class InvocationTokenAuthenticator {
34
34
  const context = {
35
35
  type: 'solid',
36
36
  webId: claims.webId,
37
+ ...(claims.podUrl ? { authorizedPodUrl: claims.podUrl } : {}),
37
38
  accountId: claims.webId,
38
39
  internalInvocation: true,
39
40
  scopes: claims.scopes,
@@ -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(',') !== 'aud,deployment,exp,iat,iss,jti,kid,scopes,v,webId'
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);