@undefineds.co/xpod 0.3.71-rc.9 → 0.3.71

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 (48) hide show
  1. package/dist/api/ai-gateway/connect/index.d.ts +2 -0
  2. package/dist/api/ai-gateway/connect/index.js +87 -24
  3. package/dist/api/ai-gateway/connect/index.js.map +1 -1
  4. package/dist/api/ai-gateway/pod/HostedPodDataAccess.js +2 -2
  5. package/dist/api/ai-gateway/pod/HostedPodDataAccess.js.map +1 -1
  6. package/dist/api/container/common.js +1 -1
  7. package/dist/api/container/common.js.map +1 -1
  8. package/dist/api/container/routes.js +1 -1
  9. package/dist/api/container/routes.js.map +1 -1
  10. package/dist/api/handlers/AiGatewayManagementHandler.js +28 -12
  11. package/dist/api/handlers/AiGatewayManagementHandler.js.map +1 -1
  12. package/dist/api/handlers/PodSettingsHandler.d.ts +12 -19
  13. package/dist/api/handlers/PodSettingsHandler.js +12 -51
  14. package/dist/api/handlers/PodSettingsHandler.js.map +1 -1
  15. package/dist/http/InternalPodDataHttpHandler.d.ts +3 -1
  16. package/dist/http/InternalPodDataHttpHandler.js +48 -7
  17. package/dist/http/InternalPodDataHttpHandler.js.map +1 -1
  18. package/dist/http/InternalPodDataHttpHandler.jsonld +10 -2
  19. package/dist/runtime/GatewayAdminProxyAuth.d.ts +1 -1
  20. package/dist/runtime/GatewayAdminProxyAuth.js +1 -1
  21. package/dist/runtime/GatewayAdminProxyAuth.js.map +1 -1
  22. package/dist/runtime/Proxy.d.ts +1 -0
  23. package/dist/runtime/Proxy.js +22 -0
  24. package/dist/runtime/Proxy.js.map +1 -1
  25. package/package.json +1 -1
  26. package/static/dashboard/assets/{NetworkPage-CXjiUMR1.js → NetworkPage-CsERgA2D.js} +1 -1
  27. package/static/dashboard/assets/{PaneListHeader-BSipfl0W.js → PaneListHeader-WJWFvTKY.js} +1 -1
  28. package/static/dashboard/assets/{PodPage-Bd3Xhmp-.js → PodPage-Bo_NbnK1.js} +1 -1
  29. package/static/dashboard/assets/{ServicesPage-CI3jWZGe.js → ServicesPage-CrNDXQlg.js} +1 -1
  30. package/static/dashboard/assets/{card-z5BV5hTo.js → card-Cjax171A.js} +1 -1
  31. package/static/dashboard/assets/{dashboard-CSrqAv2L.js → dashboard-CaE1mRUL.js} +4 -4
  32. package/static/dashboard/assets/{database-Ct5ZPe6L.js → database-B0sGiSz2.js} +1 -1
  33. package/static/dashboard/assets/{external-link-B7wD1hkm.js → external-link-C_ANDdCY.js} +1 -1
  34. package/static/dashboard/assets/{index-tOBaUnM1.js → index-CDoxbvUP.js} +1 -1
  35. package/static/dashboard/assets/{refresh-cw-CFDZPaE8.js → refresh-cw-DHzHxCme.js} +1 -1
  36. package/static/dashboard/assets/{services-status-context-DN1Icg4A.js → services-status-context-ukghtkFD.js} +1 -1
  37. package/static/dashboard/dashboard.html +1 -1
  38. package/static/settings/assets/ModelsPage-ClTKzRGm.js +1 -0
  39. package/static/settings/assets/{PaneListHeader-DMD8OJef.js → PaneListHeader-DTVY4Ig2.js} +1 -1
  40. package/static/settings/assets/{PodPage-DT4EMYhL.js → PodPage-L_JAOD-r.js} +1 -1
  41. package/static/settings/assets/{ServicesPage-CJ0ub6IH.js → ServicesPage-BwLxrSWw.js} +1 -1
  42. package/static/settings/assets/{SettingsPage-BkR7ZK18.js → SettingsPage-DUGHDPQx.js} +1 -1
  43. package/static/settings/assets/{card-Cjsia0_V.js → card-BZF0jSa_.js} +1 -1
  44. package/static/settings/assets/{services-status-context-CkuKmH5h.js → services-status-context-BnbZHZnu.js} +1 -1
  45. package/static/settings/assets/{settings-Cyyv6-SD.js → settings-BN7FyntE.js} +4 -4
  46. package/static/settings/assets/{workspace-layout-B47luv6r.js → workspace-layout-jzYHoh3_.js} +1 -1
  47. package/static/settings/settings.html +1 -1
  48. package/static/settings/assets/ModelsPage-03NPWzoX.js +0 -1
@@ -3,7 +3,8 @@ import type { ApiServer } from '../ApiServer';
3
3
  import type { PodLookupRepository } from '../../identity/drizzle/PodLookupRepository';
4
4
  import type { UsageRepository } from '../../storage/quota/UsageRepository';
5
5
  import type { AuthContext } from '../auth/AuthContext';
6
- import type { InternalPodAccessTokenProvider } from '../ai-gateway/pod/HostedPodDataAccess';
6
+ import type { GatewayDeployment } from '../ai-gateway/auth/InvocationTokenCodec';
7
+ import type { ProviderConnectionSummary } from '../ai-gateway/connect';
7
8
  export interface PodSettingsStatus {
8
9
  identity: {
9
10
  webId: string;
@@ -54,29 +55,21 @@ export interface PodSettingsHandlerOptions {
54
55
  logger?: Pick<ReturnType<typeof getLoggerFor>, 'warn' | 'error'>;
55
56
  }
56
57
  export declare function registerPodSettingsRoutes(server: ApiServer, options: PodSettingsHandlerOptions): void;
57
- export declare class DrizzlePodAiConnectionStatusReader implements PodAiConnectionStatusReader {
58
- private readonly internalPodAccess?;
59
- private readonly dbFactory;
60
- constructor(internalPodAccess?: InternalPodAccessTokenProvider | undefined, dbFactory?: (input: {
58
+ type ProviderConnectStatusSource = {
59
+ listProviders(input: {
61
60
  webId: string;
62
- podUrl: string;
63
- fetch: typeof fetch;
64
- }) => Promise<AiConnectionStatusDb>);
61
+ deployment: GatewayDeployment;
62
+ auth?: AuthContext;
63
+ }): Promise<ProviderConnectionSummary[]>;
64
+ };
65
+ export declare class ProviderConnectPodAiConnectionStatusReader implements PodAiConnectionStatusReader {
66
+ private readonly connectService;
67
+ private readonly deployment;
68
+ constructor(connectService: ProviderConnectStatusSource, deployment: GatewayDeployment);
65
69
  read({ webId, podUrl, auth }: {
66
70
  webId: string;
67
71
  podUrl?: string;
68
72
  auth?: AuthContext;
69
73
  }): Promise<PodAiConnectionStatus>;
70
74
  }
71
- type AiConnectionStatusDb = {
72
- init?: (...resources: unknown[]) => Promise<void>;
73
- select(): {
74
- from(resource: unknown): {
75
- where(condition: unknown): {
76
- execute(): Promise<unknown[]>;
77
- };
78
- execute(): Promise<unknown[]>;
79
- };
80
- };
81
- };
82
75
  export {};
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DrizzlePodAiConnectionStatusReader = void 0;
3
+ exports.ProviderConnectPodAiConnectionStatusReader = void 0;
4
4
  exports.registerPodSettingsRoutes = registerPodSettingsRoutes;
5
- const drizzle_solid_1 = require("@undefineds.co/drizzle-solid");
6
- const models_1 = require("@undefineds.co/models");
7
5
  const global_logger_factory_1 = require("global-logger-factory");
8
6
  function registerPodSettingsRoutes(server, options) {
9
7
  const logger = options.logger ?? (0, global_logger_factory_1.getLoggerFor)('PodSettingsHandler');
@@ -72,33 +70,25 @@ async function readStorageStatus(usageRepo, pod) {
72
70
  source: 'identity_usage',
73
71
  };
74
72
  }
75
- class DrizzlePodAiConnectionStatusReader {
76
- constructor(internalPodAccess, dbFactory = createAiConnectionStatusDb) {
77
- this.internalPodAccess = internalPodAccess;
78
- this.dbFactory = dbFactory;
73
+ class ProviderConnectPodAiConnectionStatusReader {
74
+ constructor(connectService, deployment) {
75
+ this.connectService = connectService;
76
+ this.deployment = deployment;
79
77
  }
80
78
  async read({ webId, podUrl, auth }) {
81
- const trustedFetch = await this.internalPodAccess?.getTrustedFetch(webId, auth);
82
- if (!trustedFetch) {
83
- return { status: 'unsupported', reason: 'not_configured' };
84
- }
85
79
  if (!podUrl) {
86
80
  return { status: 'unsupported', reason: 'pod_not_found' };
87
81
  }
88
82
  try {
89
- const db = await this.dbFactory({ webId, podUrl, fetch: trustedFetch });
90
- await db.init?.(models_1.credentialResource, models_1.indexedFileResource);
91
- const credentialRows = await db.select().from(models_1.credentialResource).where((0, drizzle_solid_1.eq)(models_1.credentialResource.status, 'active')).execute();
92
- const indexedRows = await db.select().from(models_1.indexedFileResource).execute();
93
- const lastSyncAt = latestIso([
94
- ...credentialRows.map((row) => row.lastUsedAt ?? row.lastRefreshAt),
95
- ...indexedRows.map((row) => row.indexedAt),
96
- ]);
83
+ const providers = await this.connectService.listProviders({
84
+ webId,
85
+ deployment: this.deployment,
86
+ auth,
87
+ });
97
88
  return {
98
89
  status: 'available',
99
90
  containerUrl: new URL('settings/credentials.ttl', podUrl).toString(),
100
- configuredProviders: credentialRows.length,
101
- lastSyncAt,
91
+ configuredProviders: providers.filter((provider) => provider.status !== 'disconnected').length,
102
92
  source: 'drizzle-solid',
103
93
  };
104
94
  }
@@ -107,36 +97,7 @@ class DrizzlePodAiConnectionStatusReader {
107
97
  }
108
98
  }
109
99
  }
110
- exports.DrizzlePodAiConnectionStatusReader = DrizzlePodAiConnectionStatusReader;
111
- function createAiConnectionStatusDb(input) {
112
- return Promise.resolve((0, drizzle_solid_1.drizzle)({
113
- fetch: input.fetch,
114
- info: { webId: input.webId, isLoggedIn: true },
115
- }, {
116
- podUrl: input.podUrl,
117
- schema: {
118
- credential: models_1.credentialResource,
119
- indexedFile: models_1.indexedFileResource,
120
- },
121
- }));
122
- }
123
- function latestIso(values) {
124
- const timestamps = values
125
- .map((value) => {
126
- if (value instanceof Date)
127
- return value.getTime();
128
- if (typeof value === 'string' || typeof value === 'number') {
129
- const parsed = Date.parse(String(value));
130
- return Number.isFinite(parsed) ? parsed : undefined;
131
- }
132
- return undefined;
133
- })
134
- .filter((value) => value !== undefined);
135
- if (timestamps.length === 0) {
136
- return undefined;
137
- }
138
- return new Date(Math.max(...timestamps)).toISOString();
139
- }
100
+ exports.ProviderConnectPodAiConnectionStatusReader = ProviderConnectPodAiConnectionStatusReader;
140
101
  function sendJson(response, status, data) {
141
102
  response.statusCode = status;
142
103
  response.setHeader('Content-Type', 'application/json');
@@ -1 +1 @@
1
- {"version":3,"file":"PodSettingsHandler.js","sourceRoot":"","sources":["../../../src/api/handlers/PodSettingsHandler.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ServerResponse } from 'node:http';\nimport { drizzle, eq } from '@undefineds.co/drizzle-solid';\nimport {\n credentialResource,\n indexedFileResource,\n type CredentialRow,\n type IndexedFileRow,\n} from '@undefineds.co/models';\nimport { getLoggerFor } from 'global-logger-factory';\nimport type { ApiServer } from '../ApiServer';\nimport type { AuthenticatedRequest } from '../middleware/AuthMiddleware';\nimport type { PodLookupRepository, PodLookupResult } from '../../identity/drizzle/PodLookupRepository';\nimport type { UsageRepository, PodUsageRecord } from '../../storage/quota/UsageRepository';\nimport type { AuthContext } from '../auth/AuthContext';\nimport type { InternalPodAccessTokenProvider } from '../ai-gateway/pod/HostedPodDataAccess';\n\nexport interface PodSettingsStatus {\n identity: {\n webId: string;\n podUrl?: string;\n };\n storage: PodStorageStatus;\n aiConnection: PodAiConnectionStatus;\n generatedAt: string;\n}\n\nexport type PodStorageStatus =\n | {\n status: 'available';\n usage: {\n storageBytes: number;\n ingressBytes: number;\n egressBytes: number;\n };\n limits: {\n storageLimitBytes: number | null;\n bandwidthLimitBps: number | null;\n };\n source: 'identity_usage';\n }\n | {\n status: 'unsupported' | 'error';\n reason: string;\n };\n\nexport type PodAiConnectionStatus =\n | {\n status: 'available';\n containerUrl?: string;\n configuredProviders: number;\n lastSyncAt?: string;\n source: 'drizzle-solid';\n }\n | {\n status: 'unsupported' | 'error';\n reason: string;\n };\n\nexport interface PodAiConnectionStatusReader {\n read(input: { webId: string; podUrl?: string; auth?: AuthContext }): Promise<PodAiConnectionStatus>;\n}\n\nexport interface PodSettingsHandlerOptions {\n podLookupRepository: Pick<PodLookupRepository, 'findByWebId'>;\n usageRepo: Pick<UsageRepository, 'getPodUsage'>;\n aiConnectionStatusReader?: PodAiConnectionStatusReader;\n now?: () => Date;\n logger?: Pick<ReturnType<typeof getLoggerFor>, 'warn' | 'error'>;\n}\n\nexport function registerPodSettingsRoutes(server: ApiServer, options: PodSettingsHandlerOptions): void {\n const logger = options.logger ?? getLoggerFor('PodSettingsHandler');\n const now = options.now ?? (() => new Date());\n const aiConnectionStatusReader = options.aiConnectionStatusReader ?? {\n read: async () => ({ status: 'unsupported', reason: 'not_configured' }) as const,\n };\n\n server.get('/api/pod/settings/status', async (request, response) => {\n if (!request.auth || request.auth.type !== 'solid') {\n sendJson(response, 401, { error: 'Authentication required' });\n return;\n }\n\n const webId = request.auth.webId;\n try {\n const pod = await options.podLookupRepository.findByWebId(webId);\n const podUrl = pod?.storageUrl ?? pod?.baseUrl;\n const [storage, aiConnection] = await Promise.all([\n readStorageStatus(options.usageRepo, pod),\n aiConnectionStatusReader.read({ webId, podUrl, auth: request.auth }).catch((error: unknown) => {\n const reason = safeAiConnectionFailureReason(error);\n logger.warn(`Failed to read Pod AI Connection status: ${reason}`);\n return { status: 'error', reason } as const;\n }),\n ]);\n\n sendJson(response, 200, {\n identity: {\n webId,\n podUrl,\n },\n storage,\n aiConnection,\n generatedAt: now().toISOString(),\n } satisfies PodSettingsStatus);\n } catch (error) {\n logger.error(`Failed to read Pod settings status: ${safeLogError(error)}`);\n sendJson(response, 500, { error: 'Failed to read Pod settings status' });\n }\n });\n}\n\nasync function readStorageStatus(\n usageRepo: Pick<UsageRepository, 'getPodUsage'>,\n pod: PodLookupResult | undefined,\n): Promise<PodStorageStatus> {\n if (!pod) {\n return { status: 'unsupported', reason: 'pod_not_found' };\n }\n\n let usage: PodUsageRecord | undefined;\n try {\n usage = await usageRepo.getPodUsage(pod.podId);\n } catch {\n return { status: 'error', reason: 'usage_unavailable' };\n }\n\n if (!usage) {\n return { status: 'unsupported', reason: 'usage_not_available' };\n }\n\n return {\n status: 'available',\n usage: {\n storageBytes: usage.storageBytes,\n ingressBytes: usage.ingressBytes,\n egressBytes: usage.egressBytes,\n },\n limits: {\n storageLimitBytes: usage.storageLimitBytes ?? null,\n bandwidthLimitBps: usage.bandwidthLimitBps ?? null,\n },\n source: 'identity_usage',\n };\n}\n\nexport class DrizzlePodAiConnectionStatusReader implements PodAiConnectionStatusReader {\n public constructor(\n private readonly internalPodAccess?: InternalPodAccessTokenProvider,\n private readonly dbFactory: (input: {\n webId: string;\n podUrl: string;\n fetch: typeof fetch;\n }) => Promise<AiConnectionStatusDb> = createAiConnectionStatusDb,\n ) {}\n\n public async read({ webId, podUrl, auth }: { webId: string; podUrl?: string; auth?: AuthContext }): Promise<PodAiConnectionStatus> {\n const trustedFetch = await this.internalPodAccess?.getTrustedFetch(webId, auth);\n if (!trustedFetch) {\n return { status: 'unsupported', reason: 'not_configured' };\n }\n if (!podUrl) {\n return { status: 'unsupported', reason: 'pod_not_found' };\n }\n\n try {\n const db = await this.dbFactory({ webId, podUrl, fetch: trustedFetch });\n await db.init?.(credentialResource, indexedFileResource);\n\n const credentialRows = await db.select().from(credentialResource).where(eq(credentialResource.status, 'active')).execute() as CredentialRow[];\n const indexedRows = await db.select().from(indexedFileResource).execute() as IndexedFileRow[];\n const lastSyncAt = latestIso([\n ...credentialRows.map((row) => row.lastUsedAt ?? row.lastRefreshAt),\n ...indexedRows.map((row) => row.indexedAt),\n ]);\n\n return {\n status: 'available',\n containerUrl: new URL('settings/credentials.ttl', podUrl).toString(),\n configuredProviders: credentialRows.length,\n lastSyncAt,\n source: 'drizzle-solid',\n };\n } catch {\n return { status: 'error', reason: 'ai_connection_unavailable' };\n }\n }\n}\n\ntype AiConnectionStatusDb = {\n init?: (...resources: unknown[]) => Promise<void>;\n select(): {\n from(resource: unknown): {\n where(condition: unknown): { execute(): Promise<unknown[]> };\n execute(): Promise<unknown[]>;\n };\n };\n};\n\nfunction createAiConnectionStatusDb(input: {\n webId: string;\n podUrl: string;\n fetch: typeof fetch;\n}): Promise<AiConnectionStatusDb> {\n return Promise.resolve(drizzle({\n fetch: input.fetch,\n info: { webId: input.webId, isLoggedIn: true },\n } as any, {\n podUrl: input.podUrl,\n schema: {\n credential: credentialResource,\n indexedFile: indexedFileResource,\n },\n }) as unknown as AiConnectionStatusDb);\n}\n\nfunction latestIso(values: unknown[]): string | undefined {\n const timestamps = values\n .map((value) => {\n if (value instanceof Date) return value.getTime();\n if (typeof value === 'string' || typeof value === 'number') {\n const parsed = Date.parse(String(value));\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n })\n .filter((value): value is number => value !== undefined);\n if (timestamps.length === 0) {\n return undefined;\n }\n return new Date(Math.max(...timestamps)).toISOString();\n}\n\nfunction sendJson(response: ServerResponse, status: number, data: unknown): void {\n response.statusCode = status;\n response.setHeader('Content-Type', 'application/json');\n response.end(JSON.stringify(data));\n}\n\nfunction safeLogError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction safeAiConnectionFailureReason(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n for (const reason of [\n 'service_access_missing',\n 'not_configured',\n 'pod_not_found',\n 'ai_connection_unavailable',\n ]) {\n if (message.includes(reason)) {\n return reason;\n }\n }\n return 'ai_connection_unavailable';\n}\n"]}
1
+ {"version":3,"file":"PodSettingsHandler.js","sourceRoot":"","sources":["../../../src/api/handlers/PodSettingsHandler.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ServerResponse } from 'node:http';\nimport { getLoggerFor } from 'global-logger-factory';\nimport type { ApiServer } from '../ApiServer';\nimport type { AuthenticatedRequest } from '../middleware/AuthMiddleware';\nimport type { PodLookupRepository, PodLookupResult } from '../../identity/drizzle/PodLookupRepository';\nimport type { UsageRepository, PodUsageRecord } from '../../storage/quota/UsageRepository';\nimport type { AuthContext } from '../auth/AuthContext';\nimport type { GatewayDeployment } from '../ai-gateway/auth/InvocationTokenCodec';\nimport type { ProviderConnectionSummary } from '../ai-gateway/connect';\n\nexport interface PodSettingsStatus {\n identity: {\n webId: string;\n podUrl?: string;\n };\n storage: PodStorageStatus;\n aiConnection: PodAiConnectionStatus;\n generatedAt: string;\n}\n\nexport type PodStorageStatus =\n | {\n status: 'available';\n usage: {\n storageBytes: number;\n ingressBytes: number;\n egressBytes: number;\n };\n limits: {\n storageLimitBytes: number | null;\n bandwidthLimitBps: number | null;\n };\n source: 'identity_usage';\n }\n | {\n status: 'unsupported' | 'error';\n reason: string;\n };\n\nexport type PodAiConnectionStatus =\n | {\n status: 'available';\n containerUrl?: string;\n configuredProviders: number;\n lastSyncAt?: string;\n source: 'drizzle-solid';\n }\n | {\n status: 'unsupported' | 'error';\n reason: string;\n };\n\nexport interface PodAiConnectionStatusReader {\n read(input: { webId: string; podUrl?: string; auth?: AuthContext }): Promise<PodAiConnectionStatus>;\n}\n\nexport interface PodSettingsHandlerOptions {\n podLookupRepository: Pick<PodLookupRepository, 'findByWebId'>;\n usageRepo: Pick<UsageRepository, 'getPodUsage'>;\n aiConnectionStatusReader?: PodAiConnectionStatusReader;\n now?: () => Date;\n logger?: Pick<ReturnType<typeof getLoggerFor>, 'warn' | 'error'>;\n}\n\nexport function registerPodSettingsRoutes(server: ApiServer, options: PodSettingsHandlerOptions): void {\n const logger = options.logger ?? getLoggerFor('PodSettingsHandler');\n const now = options.now ?? (() => new Date());\n const aiConnectionStatusReader = options.aiConnectionStatusReader ?? {\n read: async () => ({ status: 'unsupported', reason: 'not_configured' }) as const,\n };\n\n server.get('/api/pod/settings/status', async (request, response) => {\n if (!request.auth || request.auth.type !== 'solid') {\n sendJson(response, 401, { error: 'Authentication required' });\n return;\n }\n\n const webId = request.auth.webId;\n try {\n const pod = await options.podLookupRepository.findByWebId(webId);\n const podUrl = pod?.storageUrl ?? pod?.baseUrl;\n const [storage, aiConnection] = await Promise.all([\n readStorageStatus(options.usageRepo, pod),\n aiConnectionStatusReader.read({ webId, podUrl, auth: request.auth }).catch((error: unknown) => {\n const reason = safeAiConnectionFailureReason(error);\n logger.warn(`Failed to read Pod AI Connection status: ${reason}`);\n return { status: 'error', reason } as const;\n }),\n ]);\n\n sendJson(response, 200, {\n identity: {\n webId,\n podUrl,\n },\n storage,\n aiConnection,\n generatedAt: now().toISOString(),\n } satisfies PodSettingsStatus);\n } catch (error) {\n logger.error(`Failed to read Pod settings status: ${safeLogError(error)}`);\n sendJson(response, 500, { error: 'Failed to read Pod settings status' });\n }\n });\n}\n\nasync function readStorageStatus(\n usageRepo: Pick<UsageRepository, 'getPodUsage'>,\n pod: PodLookupResult | undefined,\n): Promise<PodStorageStatus> {\n if (!pod) {\n return { status: 'unsupported', reason: 'pod_not_found' };\n }\n\n let usage: PodUsageRecord | undefined;\n try {\n usage = await usageRepo.getPodUsage(pod.podId);\n } catch {\n return { status: 'error', reason: 'usage_unavailable' };\n }\n\n if (!usage) {\n return { status: 'unsupported', reason: 'usage_not_available' };\n }\n\n return {\n status: 'available',\n usage: {\n storageBytes: usage.storageBytes,\n ingressBytes: usage.ingressBytes,\n egressBytes: usage.egressBytes,\n },\n limits: {\n storageLimitBytes: usage.storageLimitBytes ?? null,\n bandwidthLimitBps: usage.bandwidthLimitBps ?? null,\n },\n source: 'identity_usage',\n };\n}\n\ntype ProviderConnectStatusSource = {\n listProviders(input: {\n webId: string;\n deployment: GatewayDeployment;\n auth?: AuthContext;\n }): Promise<ProviderConnectionSummary[]>;\n};\n\nexport class ProviderConnectPodAiConnectionStatusReader implements PodAiConnectionStatusReader {\n public constructor(\n private readonly connectService: ProviderConnectStatusSource,\n private readonly deployment: GatewayDeployment,\n ) {}\n\n public async read({ webId, podUrl, auth }: { webId: string; podUrl?: string; auth?: AuthContext }): Promise<PodAiConnectionStatus> {\n if (!podUrl) {\n return { status: 'unsupported', reason: 'pod_not_found' };\n }\n\n try {\n const providers = await this.connectService.listProviders({\n webId,\n deployment: this.deployment,\n auth,\n });\n\n return {\n status: 'available',\n containerUrl: new URL('settings/credentials.ttl', podUrl).toString(),\n configuredProviders: providers.filter((provider) => provider.status !== 'disconnected').length,\n source: 'drizzle-solid',\n };\n } catch {\n return { status: 'error', reason: 'ai_connection_unavailable' };\n }\n }\n}\n\nfunction sendJson(response: ServerResponse, status: number, data: unknown): void {\n response.statusCode = status;\n response.setHeader('Content-Type', 'application/json');\n response.end(JSON.stringify(data));\n}\n\nfunction safeLogError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction safeAiConnectionFailureReason(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n for (const reason of [\n 'service_access_missing',\n 'not_configured',\n 'pod_not_found',\n 'ai_connection_unavailable',\n ]) {\n if (message.includes(reason)) {\n return reason;\n }\n }\n return 'ai_connection_unavailable';\n}\n"]}
@@ -18,6 +18,7 @@ export declare class InternalPodDataHttpHandler extends HttpHandler {
18
18
  canHandle({ request }: HttpHandlerInput): Promise<void>;
19
19
  handle({ request, response }: HttpHandlerInput): Promise<void>;
20
20
  private verifyRequest;
21
+ private logRejectedRequest;
21
22
  private intentMatchesRequest;
22
23
  private isHostedOwnerResource;
23
24
  private isAllowedAiConnectionResource;
@@ -26,7 +27,8 @@ export declare class InternalPodDataHttpHandler extends HttpHandler {
26
27
  private createPatch;
27
28
  private pipeRepresentation;
28
29
  private writeNotFound;
29
- private parseUrl;
30
+ private writeEmptyGraph;
31
+ private requestPathname;
30
32
  private parseOptionalUrl;
31
33
  private isInsideDeployment;
32
34
  }
@@ -6,7 +6,7 @@ const global_logger_factory_1 = require("global-logger-factory");
6
6
  const community_server_1 = require("@solid/community-server");
7
7
  const AiConnectionServiceAccess_1 = require("../api/ai-gateway/service-access/AiConnectionServiceAccess");
8
8
  const GatewayAdminProxyAuth_1 = require("../runtime/GatewayAdminProxyAuth");
9
- const ALLOWED_METHODS = new Set(['GET', 'PUT', 'PATCH', 'DELETE']);
9
+ const ALLOWED_METHODS = new Set(['GET', 'HEAD', 'PUT', 'PATCH', 'DELETE']);
10
10
  const DEFAULT_NONCE_TTL_MS = 120_000;
11
11
  const DEFAULT_NONCE_MAX_ENTRIES = 10_000;
12
12
  class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
@@ -26,12 +26,12 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
26
26
  });
27
27
  }
28
28
  async canHandle({ request }) {
29
- if (this.parseUrl(request).pathname !== this.basePath) {
29
+ if (this.requestPathname(request) !== this.basePath) {
30
30
  throw new community_server_1.NotImplementedHttpError('Not an internal Pod data request.');
31
31
  }
32
32
  }
33
33
  async handle({ request, response }) {
34
- if (this.parseUrl(request).pathname !== this.basePath) {
34
+ if (this.requestPathname(request) !== this.basePath) {
35
35
  throw new community_server_1.NotImplementedHttpError('Not an internal Pod data request.');
36
36
  }
37
37
  const intent = this.verifyRequest(request);
@@ -44,7 +44,12 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
44
44
  }
45
45
  catch (error) {
46
46
  if (isHttpNotFound(error)) {
47
- this.writeNotFound(response);
47
+ if (intent.method === 'GET') {
48
+ this.writeEmptyGraph(response);
49
+ }
50
+ else {
51
+ this.writeNotFound(response);
52
+ }
48
53
  return;
49
54
  }
50
55
  throw error;
@@ -53,9 +58,11 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
53
58
  verifyRequest(request) {
54
59
  const method = request.method?.toUpperCase();
55
60
  if (!ALLOWED_METHODS.has(method ?? '')) {
61
+ this.logRejectedRequest('method_not_allowed');
56
62
  return undefined;
57
63
  }
58
64
  if (!(0, GatewayAdminProxyAuth_1.isLoopbackRemoteAddress)(request.socket.remoteAddress)) {
65
+ this.logRejectedRequest('non_loopback_transport');
59
66
  return undefined;
60
67
  }
61
68
  const verification = (0, GatewayAdminProxyAuth_1.verifyGatewayAdminProxyHeaders)({
@@ -65,22 +72,30 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
65
72
  url: request.url,
66
73
  });
67
74
  if (!verification.valid || !verification.originalClientLoopback || !verification.intent || !verification.nonce) {
75
+ this.logRejectedRequest(verification.reason ?? 'invalid_runtime_marker');
68
76
  return undefined;
69
77
  }
70
78
  if (!this.intentMatchesRequest(verification.intent, method)) {
79
+ this.logRejectedRequest('intent_mismatch');
71
80
  return undefined;
72
81
  }
73
82
  if (!this.isHostedOwnerResource(verification.intent)) {
83
+ this.logRejectedRequest('owner_resource_outside_deployment');
74
84
  return undefined;
75
85
  }
76
86
  if (!this.isAllowedAiConnectionResource(verification.intent)) {
87
+ this.logRejectedRequest('resource_not_allowed');
77
88
  return undefined;
78
89
  }
79
90
  if (!this.seenNonces.consume(verification.nonce)) {
91
+ this.logRejectedRequest('nonce_rejected');
80
92
  return undefined;
81
93
  }
82
94
  return verification.intent;
83
95
  }
96
+ logRejectedRequest(reason) {
97
+ this.logger.warn(`Rejected internal Pod data request: ${reason}`);
98
+ }
84
99
  intentMatchesRequest(intent, method) {
85
100
  if (intent.method !== method) {
86
101
  return false;
@@ -88,7 +103,7 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
88
103
  if (intent.scopes.length === 0) {
89
104
  return false;
90
105
  }
91
- const requiredScope = method === 'GET' ? 'ai:credentials:read' : 'ai:credentials:write';
106
+ const requiredScope = method === 'GET' || method === 'HEAD' ? 'ai:credentials:read' : 'ai:credentials:write';
92
107
  return intent.scopes.some((scope) => scope === requiredScope ||
93
108
  scope === 'ai:credentials:*' ||
94
109
  scope === 'ai:*');
@@ -143,6 +158,16 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
143
158
  await this.pipeRepresentation(representation, response);
144
159
  return;
145
160
  }
161
+ case 'HEAD': {
162
+ const representation = await this.resourceStore.getRepresentation(identifier, {
163
+ type: { 'text/turtle': 1, '*/*': 0.1 },
164
+ });
165
+ response.statusCode = 200;
166
+ response.setHeader('Content-Type', representation.metadata.contentType ?? 'text/turtle');
167
+ representation.data.destroy();
168
+ response.end();
169
+ return;
170
+ }
146
171
  case 'PUT': {
147
172
  await this.resourceStore.setRepresentation(identifier, this.createRepresentation(request, intent.resourceUrl));
148
173
  response.statusCode = 204;
@@ -182,8 +207,24 @@ class InternalPodDataHttpHandler extends community_server_1.HttpHandler {
182
207
  response.setHeader('Cache-Control', 'no-store');
183
208
  response.end('Not Found');
184
209
  }
185
- parseUrl(request) {
186
- return new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);
210
+ writeEmptyGraph(response) {
211
+ response.statusCode = 200;
212
+ response.setHeader('Content-Type', 'text/turtle');
213
+ response.setHeader('Cache-Control', 'no-store');
214
+ response.end();
215
+ }
216
+ requestPathname(request) {
217
+ const requestUrl = request.url ?? '/';
218
+ try {
219
+ // Route ownership is path-based. Do not let an untrusted or proxy-shaped
220
+ // Host header make URL parsing throw and silently skip this first handler.
221
+ return requestUrl.startsWith('/')
222
+ ? new URL(requestUrl, 'http://xpod.internal').pathname
223
+ : new URL(requestUrl).pathname;
224
+ }
225
+ catch {
226
+ return undefined;
227
+ }
187
228
  }
188
229
  parseOptionalUrl(value) {
189
230
  if (!value) {
@@ -1 +1 @@
1
- {"version":3,"file":"InternalPodDataHttpHandler.js","sourceRoot":"","sources":["../../src/http/InternalPodDataHttpHandler.ts"],"names":[],"mappings":"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","sourcesContent":["import { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { getLoggerFor } from 'global-logger-factory';\nimport {\n BasicRepresentation,\n HttpHandler,\n NotImplementedHttpError,\n RepresentationMetadata,\n type BodyParser,\n type HttpHandlerInput,\n type HttpRequest,\n type HttpResponse,\n type Patch,\n type Representation,\n type ResourceStore,\n} from '@solid/community-server';\nimport { createAiConnectionServiceAccess } from '../api/ai-gateway/service-access/AiConnectionServiceAccess';\nimport {\n isLoopbackRemoteAddress,\n verifyGatewayAdminProxyHeaders,\n type GatewayAdminProxyIntent,\n} from '../runtime/GatewayAdminProxyAuth';\n\nexport interface InternalPodDataHttpHandlerOptions {\n resourceStore: ResourceStore;\n gatewayAdminProxyAuthSecret?: string;\n patchBodyParser?: BodyParser;\n baseUrl?: string;\n basePath?: string;\n}\n\ninterface InternalPodDataHttpHandlerNonceOptions {\n nonceTtlMs?: number;\n nonceMaxEntries?: number;\n now?: () => number;\n}\n\ntype InternalPodDataMethod = 'GET' | 'PUT' | 'PATCH' | 'DELETE';\n\nconst ALLOWED_METHODS = new Set([ 'GET', 'PUT', 'PATCH', 'DELETE' ]);\nconst DEFAULT_NONCE_TTL_MS = 120_000;\nconst DEFAULT_NONCE_MAX_ENTRIES = 10_000;\n\nexport class InternalPodDataHttpHandler extends HttpHandler {\n protected readonly logger = getLoggerFor(this);\n private readonly resourceStore: ResourceStore;\n private readonly gatewayAdminProxyAuthSecret?: string;\n private readonly patchBodyParser?: BodyParser;\n private readonly deploymentBaseUrl?: URL;\n private readonly basePath: string;\n private readonly seenNonces: BoundedTtlNonceCache;\n\n public constructor(options: InternalPodDataHttpHandlerOptions) {\n super();\n this.resourceStore = options.resourceStore;\n this.gatewayAdminProxyAuthSecret = options.gatewayAdminProxyAuthSecret ?? process.env.XPOD_GATEWAY_ADMIN_PROXY_AUTH_SECRET;\n this.patchBodyParser = options.patchBodyParser;\n this.deploymentBaseUrl = this.parseOptionalUrl(options.baseUrl ?? process.env.CSS_BASE_URL);\n this.basePath = options.basePath ?? '/.internal/pod-data';\n const nonceOptions = options as InternalPodDataHttpHandlerOptions & InternalPodDataHttpHandlerNonceOptions;\n this.seenNonces = new BoundedTtlNonceCache({\n ttlMs: nonceOptions.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS,\n maxEntries: nonceOptions.nonceMaxEntries ?? DEFAULT_NONCE_MAX_ENTRIES,\n now: nonceOptions.now,\n });\n }\n\n public override async canHandle({ request }: HttpHandlerInput): Promise<void> {\n if (this.parseUrl(request).pathname !== this.basePath) {\n throw new NotImplementedHttpError('Not an internal Pod data request.');\n }\n }\n\n public override async handle({ request, response }: HttpHandlerInput): Promise<void> {\n if (this.parseUrl(request).pathname !== this.basePath) {\n throw new NotImplementedHttpError('Not an internal Pod data request.');\n }\n\n const intent = this.verifyRequest(request);\n if (!intent) {\n this.writeNotFound(response);\n return;\n }\n\n try {\n await this.delegate(request, response, intent);\n } catch (error: unknown) {\n if (isHttpNotFound(error)) {\n this.writeNotFound(response);\n return;\n }\n throw error;\n }\n }\n\n private verifyRequest(request: HttpRequest): GatewayAdminProxyIntent | undefined {\n const method = request.method?.toUpperCase();\n if (!ALLOWED_METHODS.has(method ?? '')) {\n return undefined;\n }\n if (!isLoopbackRemoteAddress(request.socket.remoteAddress)) {\n return undefined;\n }\n\n const verification = verifyGatewayAdminProxyHeaders({\n headers: request.headers,\n secret: this.gatewayAdminProxyAuthSecret,\n method,\n url: request.url,\n });\n if (!verification.valid || !verification.originalClientLoopback || !verification.intent || !verification.nonce) {\n return undefined;\n }\n if (!this.intentMatchesRequest(verification.intent, method as InternalPodDataMethod)) {\n return undefined;\n }\n if (!this.isHostedOwnerResource(verification.intent)) {\n return undefined;\n }\n if (!this.isAllowedAiConnectionResource(verification.intent)) {\n return undefined;\n }\n if (!this.seenNonces.consume(verification.nonce)) {\n return undefined;\n }\n return verification.intent;\n }\n\n private intentMatchesRequest(intent: GatewayAdminProxyIntent, method: InternalPodDataMethod): boolean {\n if (intent.method !== method) {\n return false;\n }\n if (intent.scopes.length === 0) {\n return false;\n }\n const requiredScope = method === 'GET' ? 'ai:credentials:read' : 'ai:credentials:write';\n return intent.scopes.some((scope) =>\n scope === requiredScope ||\n scope === 'ai:credentials:*' ||\n scope === 'ai:*',\n );\n }\n\n private isHostedOwnerResource(intent: GatewayAdminProxyIntent): boolean {\n if (!this.deploymentBaseUrl) {\n return false;\n }\n\n let ownerUrl: URL;\n let resourceUrl: URL;\n try {\n ownerUrl = new URL(intent.ownerWebId);\n resourceUrl = new URL(intent.resourceUrl);\n } catch {\n return false;\n }\n if (!this.isInsideDeployment(ownerUrl) || !this.isInsideDeployment(resourceUrl)) {\n return false;\n }\n\n const podRoot = hostedPodRootFromOwner(ownerUrl);\n return podRoot !== undefined &&\n resourceUrl.origin === podRoot.origin &&\n resourceUrl.pathname.startsWith(podRoot.pathname);\n }\n\n private isAllowedAiConnectionResource(intent: GatewayAdminProxyIntent): boolean {\n let resourceUrl: URL;\n try {\n resourceUrl = new URL(intent.resourceUrl);\n } catch {\n return false;\n }\n if (resourceUrl.hash || resourceUrl.search) {\n return false;\n }\n const allowed = createAiConnectionServiceAccess({\n ownerWebId: intent.ownerWebId,\n serviceWebId: intent.ownerWebId,\n }).resources.map((resource) => resource.url);\n return allowed.includes(resourceUrl.href);\n }\n\n private async delegate(\n request: HttpRequest,\n response: HttpResponse,\n intent: GatewayAdminProxyIntent,\n ): Promise<void> {\n const identifier = { path: intent.resourceUrl };\n switch (intent.method) {\n case 'GET': {\n const representation = await this.resourceStore.getRepresentation(identifier, {\n type: { 'text/turtle': 1, '*/*': 0.1 },\n });\n response.statusCode = 200;\n response.setHeader('Content-Type', representation.metadata.contentType ?? 'text/turtle');\n await this.pipeRepresentation(representation, response);\n return;\n }\n case 'PUT': {\n await this.resourceStore.setRepresentation(identifier, this.createRepresentation(request, intent.resourceUrl));\n response.statusCode = 204;\n response.end();\n return;\n }\n case 'PATCH': {\n await this.resourceStore.modifyResource(identifier, await this.createPatch(request, intent.resourceUrl));\n response.statusCode = 204;\n response.end();\n return;\n }\n case 'DELETE': {\n await this.resourceStore.deleteResource(identifier);\n response.statusCode = 204;\n response.end();\n }\n }\n }\n\n private createRepresentation(request: HttpRequest, resourceUrl: string): Representation {\n const contentType = firstHeader(request.headers['content-type']) ?? 'text/turtle';\n return new BasicRepresentation(\n request,\n new RepresentationMetadata({ path: resourceUrl }, contentType),\n );\n }\n\n private async createPatch(request: HttpRequest, resourceUrl: string): Promise<Patch> {\n if (!this.patchBodyParser) {\n throw new NotImplementedHttpError('Internal Pod data PATCH requires a PatchBodyParser.');\n }\n const contentType = firstHeader(request.headers['content-type']) ?? 'application/sparql-update';\n const metadata = new RepresentationMetadata({ path: resourceUrl }, contentType);\n return this.patchBodyParser.handleSafe({ request, metadata }) as Promise<Patch>;\n }\n\n private async pipeRepresentation(representation: Representation, response: HttpResponse): Promise<void> {\n await pipeline(representation.data as Readable, response);\n }\n\n private writeNotFound(response: HttpResponse): void {\n response.statusCode = 404;\n response.setHeader('Cache-Control', 'no-store');\n response.end('Not Found');\n }\n\n private parseUrl(request: HttpRequest): URL {\n return new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`);\n }\n\n private parseOptionalUrl(value: string | undefined): URL | undefined {\n if (!value) {\n return undefined;\n }\n try {\n return new URL(ensureTrailingSlash(value));\n } catch {\n return undefined;\n }\n }\n\n private isInsideDeployment(url: URL): boolean {\n return this.deploymentBaseUrl !== undefined &&\n url.origin === this.deploymentBaseUrl.origin &&\n url.pathname.startsWith(this.deploymentBaseUrl.pathname);\n }\n}\n\nfunction isHttpNotFound(error: unknown): boolean {\n return typeof error === 'object' &&\n error !== null &&\n ('statusCode' in error ? error.statusCode === 404 : (error as { name?: string }).name === 'NotFoundHttpError');\n}\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction hostedPodRootFromOwner(ownerUrl: URL): URL | undefined {\n if (ownerUrl.hash !== '#me') {\n return undefined;\n }\n if (!ownerUrl.pathname.endsWith('/profile/card')) {\n return undefined;\n }\n const podPath = ownerUrl.pathname.slice(0, -'profile/card'.length);\n if (!podPath || !podPath.endsWith('/')) {\n return undefined;\n }\n return new URL(podPath, ownerUrl.origin);\n}\n\nfunction ensureTrailingSlash(value: string): string {\n return value.endsWith('/') ? value : `${value}/`;\n}\n\nclass BoundedTtlNonceCache {\n private readonly ttlMs: number;\n private readonly maxEntries: number;\n private readonly now: () => number;\n private readonly entries = new Map<string, number>();\n\n public constructor(options: { ttlMs: number; maxEntries: number; now?: () => number }) {\n this.ttlMs = Math.max(1, options.ttlMs);\n this.maxEntries = Math.max(1, options.maxEntries);\n this.now = options.now ?? Date.now;\n }\n\n public consume(nonce: string): boolean {\n const now = this.now();\n this.pruneExpired(now);\n if (this.entries.has(nonce)) {\n return false;\n }\n if (this.entries.size >= this.maxEntries) {\n return false;\n }\n this.entries.set(nonce, now + this.ttlMs);\n return true;\n }\n\n private pruneExpired(now: number): void {\n for (const [nonce, expiresAt] of this.entries) {\n if (expiresAt > now) {\n continue;\n }\n this.entries.delete(nonce);\n }\n }\n\n}\n"]}
1
+ {"version":3,"file":"InternalPodDataHttpHandler.js","sourceRoot":"","sources":["../../src/http/InternalPodDataHttpHandler.ts"],"names":[],"mappings":"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","sourcesContent":["import { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { getLoggerFor } from 'global-logger-factory';\nimport {\n BasicRepresentation,\n HttpHandler,\n NotImplementedHttpError,\n RepresentationMetadata,\n type BodyParser,\n type HttpHandlerInput,\n type HttpRequest,\n type HttpResponse,\n type Patch,\n type Representation,\n type ResourceStore,\n} from '@solid/community-server';\nimport { createAiConnectionServiceAccess } from '../api/ai-gateway/service-access/AiConnectionServiceAccess';\nimport {\n isLoopbackRemoteAddress,\n verifyGatewayAdminProxyHeaders,\n type GatewayAdminProxyIntent,\n} from '../runtime/GatewayAdminProxyAuth';\n\nexport interface InternalPodDataHttpHandlerOptions {\n resourceStore: ResourceStore;\n gatewayAdminProxyAuthSecret?: string;\n patchBodyParser?: BodyParser;\n baseUrl?: string;\n basePath?: string;\n}\n\ninterface InternalPodDataHttpHandlerNonceOptions {\n nonceTtlMs?: number;\n nonceMaxEntries?: number;\n now?: () => number;\n}\n\ntype InternalPodDataMethod = 'GET' | 'HEAD' | 'PUT' | 'PATCH' | 'DELETE';\n\nconst ALLOWED_METHODS = new Set([ 'GET', 'HEAD', 'PUT', 'PATCH', 'DELETE' ]);\nconst DEFAULT_NONCE_TTL_MS = 120_000;\nconst DEFAULT_NONCE_MAX_ENTRIES = 10_000;\n\nexport class InternalPodDataHttpHandler extends HttpHandler {\n protected readonly logger = getLoggerFor(this);\n private readonly resourceStore: ResourceStore;\n private readonly gatewayAdminProxyAuthSecret?: string;\n private readonly patchBodyParser?: BodyParser;\n private readonly deploymentBaseUrl?: URL;\n private readonly basePath: string;\n private readonly seenNonces: BoundedTtlNonceCache;\n\n public constructor(options: InternalPodDataHttpHandlerOptions) {\n super();\n this.resourceStore = options.resourceStore;\n this.gatewayAdminProxyAuthSecret = options.gatewayAdminProxyAuthSecret ?? process.env.XPOD_GATEWAY_ADMIN_PROXY_AUTH_SECRET;\n this.patchBodyParser = options.patchBodyParser;\n this.deploymentBaseUrl = this.parseOptionalUrl(options.baseUrl ?? process.env.CSS_BASE_URL);\n this.basePath = options.basePath ?? '/.internal/pod-data';\n const nonceOptions = options as InternalPodDataHttpHandlerOptions & InternalPodDataHttpHandlerNonceOptions;\n this.seenNonces = new BoundedTtlNonceCache({\n ttlMs: nonceOptions.nonceTtlMs ?? DEFAULT_NONCE_TTL_MS,\n maxEntries: nonceOptions.nonceMaxEntries ?? DEFAULT_NONCE_MAX_ENTRIES,\n now: nonceOptions.now,\n });\n }\n\n public override async canHandle({ request }: HttpHandlerInput): Promise<void> {\n if (this.requestPathname(request) !== this.basePath) {\n throw new NotImplementedHttpError('Not an internal Pod data request.');\n }\n }\n\n public override async handle({ request, response }: HttpHandlerInput): Promise<void> {\n if (this.requestPathname(request) !== this.basePath) {\n throw new NotImplementedHttpError('Not an internal Pod data request.');\n }\n\n const intent = this.verifyRequest(request);\n if (!intent) {\n this.writeNotFound(response);\n return;\n }\n\n try {\n await this.delegate(request, response, intent);\n } catch (error: unknown) {\n if (isHttpNotFound(error)) {\n if (intent.method === 'GET') {\n this.writeEmptyGraph(response);\n } else {\n this.writeNotFound(response);\n }\n return;\n }\n throw error;\n }\n }\n\n private verifyRequest(request: HttpRequest): GatewayAdminProxyIntent | undefined {\n const method = request.method?.toUpperCase();\n if (!ALLOWED_METHODS.has(method ?? '')) {\n this.logRejectedRequest('method_not_allowed');\n return undefined;\n }\n if (!isLoopbackRemoteAddress(request.socket.remoteAddress)) {\n this.logRejectedRequest('non_loopback_transport');\n return undefined;\n }\n\n const verification = verifyGatewayAdminProxyHeaders({\n headers: request.headers,\n secret: this.gatewayAdminProxyAuthSecret,\n method,\n url: request.url,\n });\n if (!verification.valid || !verification.originalClientLoopback || !verification.intent || !verification.nonce) {\n this.logRejectedRequest(verification.reason ?? 'invalid_runtime_marker');\n return undefined;\n }\n if (!this.intentMatchesRequest(verification.intent, method as InternalPodDataMethod)) {\n this.logRejectedRequest('intent_mismatch');\n return undefined;\n }\n if (!this.isHostedOwnerResource(verification.intent)) {\n this.logRejectedRequest('owner_resource_outside_deployment');\n return undefined;\n }\n if (!this.isAllowedAiConnectionResource(verification.intent)) {\n this.logRejectedRequest('resource_not_allowed');\n return undefined;\n }\n if (!this.seenNonces.consume(verification.nonce)) {\n this.logRejectedRequest('nonce_rejected');\n return undefined;\n }\n return verification.intent;\n }\n\n private logRejectedRequest(reason: string): void {\n this.logger.warn(`Rejected internal Pod data request: ${reason}`);\n }\n\n private intentMatchesRequest(intent: GatewayAdminProxyIntent, method: InternalPodDataMethod): boolean {\n if (intent.method !== method) {\n return false;\n }\n if (intent.scopes.length === 0) {\n return false;\n }\n const requiredScope = method === 'GET' || method === 'HEAD' ? 'ai:credentials:read' : 'ai:credentials:write';\n return intent.scopes.some((scope) =>\n scope === requiredScope ||\n scope === 'ai:credentials:*' ||\n scope === 'ai:*',\n );\n }\n\n private isHostedOwnerResource(intent: GatewayAdminProxyIntent): boolean {\n if (!this.deploymentBaseUrl) {\n return false;\n }\n\n let ownerUrl: URL;\n let resourceUrl: URL;\n try {\n ownerUrl = new URL(intent.ownerWebId);\n resourceUrl = new URL(intent.resourceUrl);\n } catch {\n return false;\n }\n if (!this.isInsideDeployment(ownerUrl) || !this.isInsideDeployment(resourceUrl)) {\n return false;\n }\n\n const podRoot = hostedPodRootFromOwner(ownerUrl);\n return podRoot !== undefined &&\n resourceUrl.origin === podRoot.origin &&\n resourceUrl.pathname.startsWith(podRoot.pathname);\n }\n\n private isAllowedAiConnectionResource(intent: GatewayAdminProxyIntent): boolean {\n let resourceUrl: URL;\n try {\n resourceUrl = new URL(intent.resourceUrl);\n } catch {\n return false;\n }\n if (resourceUrl.hash || resourceUrl.search) {\n return false;\n }\n const allowed = createAiConnectionServiceAccess({\n ownerWebId: intent.ownerWebId,\n serviceWebId: intent.ownerWebId,\n }).resources.map((resource) => resource.url);\n return allowed.includes(resourceUrl.href);\n }\n\n private async delegate(\n request: HttpRequest,\n response: HttpResponse,\n intent: GatewayAdminProxyIntent,\n ): Promise<void> {\n const identifier = { path: intent.resourceUrl };\n switch (intent.method) {\n case 'GET': {\n const representation = await this.resourceStore.getRepresentation(identifier, {\n type: { 'text/turtle': 1, '*/*': 0.1 },\n });\n response.statusCode = 200;\n response.setHeader('Content-Type', representation.metadata.contentType ?? 'text/turtle');\n await this.pipeRepresentation(representation, response);\n return;\n }\n case 'HEAD': {\n const representation = await this.resourceStore.getRepresentation(identifier, {\n type: { 'text/turtle': 1, '*/*': 0.1 },\n });\n response.statusCode = 200;\n response.setHeader('Content-Type', representation.metadata.contentType ?? 'text/turtle');\n (representation.data as Readable).destroy();\n response.end();\n return;\n }\n case 'PUT': {\n await this.resourceStore.setRepresentation(identifier, this.createRepresentation(request, intent.resourceUrl));\n response.statusCode = 204;\n response.end();\n return;\n }\n case 'PATCH': {\n await this.resourceStore.modifyResource(identifier, await this.createPatch(request, intent.resourceUrl));\n response.statusCode = 204;\n response.end();\n return;\n }\n case 'DELETE': {\n await this.resourceStore.deleteResource(identifier);\n response.statusCode = 204;\n response.end();\n }\n }\n }\n\n private createRepresentation(request: HttpRequest, resourceUrl: string): Representation {\n const contentType = firstHeader(request.headers['content-type']) ?? 'text/turtle';\n return new BasicRepresentation(\n request,\n new RepresentationMetadata({ path: resourceUrl }, contentType),\n );\n }\n\n private async createPatch(request: HttpRequest, resourceUrl: string): Promise<Patch> {\n if (!this.patchBodyParser) {\n throw new NotImplementedHttpError('Internal Pod data PATCH requires a PatchBodyParser.');\n }\n const contentType = firstHeader(request.headers['content-type']) ?? 'application/sparql-update';\n const metadata = new RepresentationMetadata({ path: resourceUrl }, contentType);\n return this.patchBodyParser.handleSafe({ request, metadata }) as Promise<Patch>;\n }\n\n private async pipeRepresentation(representation: Representation, response: HttpResponse): Promise<void> {\n await pipeline(representation.data as Readable, response);\n }\n\n private writeNotFound(response: HttpResponse): void {\n response.statusCode = 404;\n response.setHeader('Cache-Control', 'no-store');\n response.end('Not Found');\n }\n\n private writeEmptyGraph(response: HttpResponse): void {\n response.statusCode = 200;\n response.setHeader('Content-Type', 'text/turtle');\n response.setHeader('Cache-Control', 'no-store');\n response.end();\n }\n\n private requestPathname(request: HttpRequest): string | undefined {\n const requestUrl = request.url ?? '/';\n try {\n // Route ownership is path-based. Do not let an untrusted or proxy-shaped\n // Host header make URL parsing throw and silently skip this first handler.\n return requestUrl.startsWith('/')\n ? new URL(requestUrl, 'http://xpod.internal').pathname\n : new URL(requestUrl).pathname;\n } catch {\n return undefined;\n }\n }\n\n private parseOptionalUrl(value: string | undefined): URL | undefined {\n if (!value) {\n return undefined;\n }\n try {\n return new URL(ensureTrailingSlash(value));\n } catch {\n return undefined;\n }\n }\n\n private isInsideDeployment(url: URL): boolean {\n return this.deploymentBaseUrl !== undefined &&\n url.origin === this.deploymentBaseUrl.origin &&\n url.pathname.startsWith(this.deploymentBaseUrl.pathname);\n }\n}\n\nfunction isHttpNotFound(error: unknown): boolean {\n return typeof error === 'object' &&\n error !== null &&\n ('statusCode' in error ? error.statusCode === 404 : (error as { name?: string }).name === 'NotFoundHttpError');\n}\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction hostedPodRootFromOwner(ownerUrl: URL): URL | undefined {\n if (ownerUrl.hash !== '#me') {\n return undefined;\n }\n if (!ownerUrl.pathname.endsWith('/profile/card')) {\n return undefined;\n }\n const podPath = ownerUrl.pathname.slice(0, -'profile/card'.length);\n if (!podPath || !podPath.endsWith('/')) {\n return undefined;\n }\n return new URL(podPath, ownerUrl.origin);\n}\n\nfunction ensureTrailingSlash(value: string): string {\n return value.endsWith('/') ? value : `${value}/`;\n}\n\nclass BoundedTtlNonceCache {\n private readonly ttlMs: number;\n private readonly maxEntries: number;\n private readonly now: () => number;\n private readonly entries = new Map<string, number>();\n\n public constructor(options: { ttlMs: number; maxEntries: number; now?: () => number }) {\n this.ttlMs = Math.max(1, options.ttlMs);\n this.maxEntries = Math.max(1, options.maxEntries);\n this.now = options.now ?? Date.now;\n }\n\n public consume(nonce: string): boolean {\n const now = this.now();\n this.pruneExpired(now);\n if (this.entries.has(nonce)) {\n return false;\n }\n if (this.entries.size >= this.maxEntries) {\n return false;\n }\n this.entries.set(nonce, now + this.ttlMs);\n return true;\n }\n\n private pruneExpired(now: number): void {\n for (const [nonce, expiresAt] of this.entries) {\n if (expiresAt > now) {\n continue;\n }\n this.entries.delete(nonce);\n }\n }\n\n}\n"]}
@@ -114,6 +114,10 @@
114
114
  "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_verifyRequest",
115
115
  "memberFieldName": "verifyRequest"
116
116
  },
117
+ {
118
+ "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_logRejectedRequest",
119
+ "memberFieldName": "logRejectedRequest"
120
+ },
117
121
  {
118
122
  "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_intentMatchesRequest",
119
123
  "memberFieldName": "intentMatchesRequest"
@@ -147,8 +151,12 @@
147
151
  "memberFieldName": "writeNotFound"
148
152
  },
149
153
  {
150
- "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_parseUrl",
151
- "memberFieldName": "parseUrl"
154
+ "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_writeEmptyGraph",
155
+ "memberFieldName": "writeEmptyGraph"
156
+ },
157
+ {
158
+ "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_requestPathname",
159
+ "memberFieldName": "requestPathname"
152
160
  },
153
161
  {
154
162
  "@id": "undefineds:dist/http/InternalPodDataHttpHandler.jsonld#InternalPodDataHttpHandler__member_parseOptionalUrl",
@@ -25,7 +25,7 @@ export interface GatewayAdminProxyMarkerVerification {
25
25
  }
26
26
  export interface GatewayAdminProxyIntent {
27
27
  ownerWebId: string;
28
- method: 'GET' | 'PUT' | 'PATCH' | 'DELETE';
28
+ method: 'GET' | 'HEAD' | 'PUT' | 'PATCH' | 'DELETE';
29
29
  resourceUrl: string;
30
30
  principalKind: 'solid-user';
31
31
  scopes: string[];
@@ -134,7 +134,7 @@ function parseGatewayAdminProxyIntent(value) {
134
134
  const parsed = JSON.parse(value);
135
135
  if (typeof parsed.ownerWebId !== 'string' ||
136
136
  typeof parsed.resourceUrl !== 'string' ||
137
- (parsed.method !== 'GET' && parsed.method !== 'PUT' && parsed.method !== 'PATCH' && parsed.method !== 'DELETE') ||
137
+ (parsed.method !== 'GET' && parsed.method !== 'HEAD' && parsed.method !== 'PUT' && parsed.method !== 'PATCH' && parsed.method !== 'DELETE') ||
138
138
  parsed.principalKind !== 'solid-user' ||
139
139
  !Array.isArray(parsed.scopes) ||
140
140
  !parsed.scopes.every((scope) => typeof scope === 'string')) {
@@ -1 +1 @@
1
- {"version":3,"file":"GatewayAdminProxyAuth.js","sourceRoot":"","sources":["../../src/runtime/GatewayAdminProxyAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'node:http';\nimport { isIP } from 'node:net';\n\nexport const GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER = 'x-xpod-admin-proxy-loopback';\nexport const GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER = 'x-xpod-admin-proxy-timestamp';\nexport const GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER = 'x-xpod-admin-proxy-signature';\nexport const GATEWAY_ADMIN_PROXY_INTENT_HEADER = 'x-xpod-admin-proxy-intent';\nexport const GATEWAY_ADMIN_PROXY_NONCE_HEADER = 'x-xpod-admin-proxy-nonce';\n\nexport const GATEWAY_ADMIN_PROXY_HEADERS = [\n GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER,\n GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER,\n GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER,\n GATEWAY_ADMIN_PROXY_INTENT_HEADER,\n GATEWAY_ADMIN_PROXY_NONCE_HEADER,\n] as const;\n\nconst MAX_CLOCK_SKEW_MS = 60_000;\n\nexport function createGatewayAdminProxyAuthSecret(): string {\n return randomBytes(32).toString('base64url');\n}\n\nexport interface GatewayAdminProxyMarkerInput {\n secret: string;\n method: string | undefined;\n url: string | undefined;\n originalClientLoopback: boolean;\n issuedAt?: number;\n nonce?: string;\n intent?: GatewayAdminProxyIntent;\n}\n\nexport interface GatewayAdminProxyMarkerVerification {\n present: boolean;\n valid: boolean;\n originalClientLoopback: boolean;\n nonce?: string;\n intent?: GatewayAdminProxyIntent;\n reason?: string;\n}\n\nexport interface GatewayAdminProxyIntent {\n ownerWebId: string;\n method: 'GET' | 'PUT' | 'PATCH' | 'DELETE';\n resourceUrl: string;\n principalKind: 'solid-user';\n scopes: string[];\n}\n\nexport function createGatewayAdminProxyHeaders(input: GatewayAdminProxyMarkerInput): OutgoingHttpHeaders {\n const issuedAt = input.issuedAt ?? Date.now();\n const loopback = input.originalClientLoopback ? '1' : '0';\n const intent = input.intent ? canonicalGatewayAdminProxyIntent(input.intent) : undefined;\n return {\n [GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER]: loopback,\n [GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER]: String(issuedAt),\n ...(input.nonce ? { [GATEWAY_ADMIN_PROXY_NONCE_HEADER]: input.nonce } : {}),\n ...(intent ? { [GATEWAY_ADMIN_PROXY_INTENT_HEADER]: intent } : {}),\n [GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER]: signGatewayAdminProxyMarker({\n secret: input.secret,\n method: input.method,\n url: input.url,\n loopback,\n issuedAt,\n nonce: input.nonce,\n intent,\n }),\n };\n}\n\nexport function stripGatewayAdminProxyHeaders(headers: IncomingHttpHeaders): void {\n for (const header of GATEWAY_ADMIN_PROXY_HEADERS) {\n delete headers[header];\n }\n}\n\nexport function isLoopbackRemoteAddress(remoteAddress: string | undefined): boolean {\n if (!remoteAddress) return false;\n const normalized = remoteAddress.startsWith('::ffff:')\n ? remoteAddress.slice('::ffff:'.length)\n : remoteAddress;\n\n if (isIP(normalized) === 4) {\n return normalized.startsWith('127.');\n }\n return normalized === '::1';\n}\n\nexport function verifyGatewayAdminProxyHeaders(input: {\n headers: IncomingHttpHeaders;\n secret?: string;\n method: string | undefined;\n url: string | undefined;\n now?: number;\n}): GatewayAdminProxyMarkerVerification {\n const loopback = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER]);\n const timestamp = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER]);\n const signature = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER]);\n const nonce = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_NONCE_HEADER]);\n const intentHeader = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_INTENT_HEADER]);\n const present = loopback !== undefined || timestamp !== undefined || signature !== undefined;\n if (!present) {\n return { present: false, valid: false, originalClientLoopback: false };\n }\n if (!input.secret) {\n return { present: true, valid: false, originalClientLoopback: false, reason: 'missing_secret' };\n }\n if (loopback !== '0' && loopback !== '1') {\n return { present: true, valid: false, originalClientLoopback: false, reason: 'invalid_loopback' };\n }\n if (!timestamp || !signature) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'incomplete_marker' };\n }\n\n const issuedAt = Number.parseInt(timestamp, 10);\n if (!Number.isFinite(issuedAt)) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'invalid_timestamp' };\n }\n if (Math.abs((input.now ?? Date.now()) - issuedAt) > MAX_CLOCK_SKEW_MS) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'expired_marker' };\n }\n\n const intent = parseGatewayAdminProxyIntent(intentHeader);\n if (intentHeader !== undefined && !intent) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'invalid_intent' };\n }\n\n const expected = signGatewayAdminProxyMarker({\n secret: input.secret,\n method: input.method,\n url: input.url,\n loopback,\n issuedAt,\n nonce,\n intent: intentHeader,\n });\n if (!safeEqual(signature, expected)) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'bad_signature' };\n }\n\n return { present: true, valid: true, originalClientLoopback: loopback === '1', nonce, intent };\n}\n\nfunction signGatewayAdminProxyMarker(input: {\n secret: string;\n method: string | undefined;\n url: string | undefined;\n loopback: '0' | '1';\n issuedAt: number;\n nonce?: string;\n intent?: string;\n}): string {\n const payload = [\n 'v1',\n (input.method ?? 'GET').toUpperCase(),\n input.url ?? '/',\n input.loopback,\n String(input.issuedAt),\n input.nonce ?? '',\n input.intent ?? '',\n ].join('\\n');\n return createHmac('sha256', input.secret).update(payload).digest('base64url');\n}\n\nexport function canonicalGatewayAdminProxyIntent(intent: GatewayAdminProxyIntent): string {\n return JSON.stringify({\n ownerWebId: intent.ownerWebId,\n method: intent.method,\n resourceUrl: intent.resourceUrl,\n principalKind: intent.principalKind,\n scopes: [...intent.scopes].sort(),\n });\n}\n\nfunction parseGatewayAdminProxyIntent(value: string | undefined): GatewayAdminProxyIntent | undefined {\n if (!value) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(value) as Partial<GatewayAdminProxyIntent>;\n if (typeof parsed.ownerWebId !== 'string' ||\n typeof parsed.resourceUrl !== 'string' ||\n (parsed.method !== 'GET' && parsed.method !== 'PUT' && parsed.method !== 'PATCH' && parsed.method !== 'DELETE') ||\n parsed.principalKind !== 'solid-user' ||\n !Array.isArray(parsed.scopes) ||\n !parsed.scopes.every((scope) => typeof scope === 'string')) {\n return undefined;\n }\n return {\n ownerWebId: parsed.ownerWebId,\n method: parsed.method,\n resourceUrl: parsed.resourceUrl,\n principalKind: parsed.principalKind,\n scopes: parsed.scopes,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction safeEqual(actual: string, expected: string): boolean {\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);\n}\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n"]}
1
+ {"version":3,"file":"GatewayAdminProxyAuth.js","sourceRoot":"","sources":["../../src/runtime/GatewayAdminProxyAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';\nimport type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'node:http';\nimport { isIP } from 'node:net';\n\nexport const GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER = 'x-xpod-admin-proxy-loopback';\nexport const GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER = 'x-xpod-admin-proxy-timestamp';\nexport const GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER = 'x-xpod-admin-proxy-signature';\nexport const GATEWAY_ADMIN_PROXY_INTENT_HEADER = 'x-xpod-admin-proxy-intent';\nexport const GATEWAY_ADMIN_PROXY_NONCE_HEADER = 'x-xpod-admin-proxy-nonce';\n\nexport const GATEWAY_ADMIN_PROXY_HEADERS = [\n GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER,\n GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER,\n GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER,\n GATEWAY_ADMIN_PROXY_INTENT_HEADER,\n GATEWAY_ADMIN_PROXY_NONCE_HEADER,\n] as const;\n\nconst MAX_CLOCK_SKEW_MS = 60_000;\n\nexport function createGatewayAdminProxyAuthSecret(): string {\n return randomBytes(32).toString('base64url');\n}\n\nexport interface GatewayAdminProxyMarkerInput {\n secret: string;\n method: string | undefined;\n url: string | undefined;\n originalClientLoopback: boolean;\n issuedAt?: number;\n nonce?: string;\n intent?: GatewayAdminProxyIntent;\n}\n\nexport interface GatewayAdminProxyMarkerVerification {\n present: boolean;\n valid: boolean;\n originalClientLoopback: boolean;\n nonce?: string;\n intent?: GatewayAdminProxyIntent;\n reason?: string;\n}\n\nexport interface GatewayAdminProxyIntent {\n ownerWebId: string;\n method: 'GET' | 'HEAD' | 'PUT' | 'PATCH' | 'DELETE';\n resourceUrl: string;\n principalKind: 'solid-user';\n scopes: string[];\n}\n\nexport function createGatewayAdminProxyHeaders(input: GatewayAdminProxyMarkerInput): OutgoingHttpHeaders {\n const issuedAt = input.issuedAt ?? Date.now();\n const loopback = input.originalClientLoopback ? '1' : '0';\n const intent = input.intent ? canonicalGatewayAdminProxyIntent(input.intent) : undefined;\n return {\n [GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER]: loopback,\n [GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER]: String(issuedAt),\n ...(input.nonce ? { [GATEWAY_ADMIN_PROXY_NONCE_HEADER]: input.nonce } : {}),\n ...(intent ? { [GATEWAY_ADMIN_PROXY_INTENT_HEADER]: intent } : {}),\n [GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER]: signGatewayAdminProxyMarker({\n secret: input.secret,\n method: input.method,\n url: input.url,\n loopback,\n issuedAt,\n nonce: input.nonce,\n intent,\n }),\n };\n}\n\nexport function stripGatewayAdminProxyHeaders(headers: IncomingHttpHeaders): void {\n for (const header of GATEWAY_ADMIN_PROXY_HEADERS) {\n delete headers[header];\n }\n}\n\nexport function isLoopbackRemoteAddress(remoteAddress: string | undefined): boolean {\n if (!remoteAddress) return false;\n const normalized = remoteAddress.startsWith('::ffff:')\n ? remoteAddress.slice('::ffff:'.length)\n : remoteAddress;\n\n if (isIP(normalized) === 4) {\n return normalized.startsWith('127.');\n }\n return normalized === '::1';\n}\n\nexport function verifyGatewayAdminProxyHeaders(input: {\n headers: IncomingHttpHeaders;\n secret?: string;\n method: string | undefined;\n url: string | undefined;\n now?: number;\n}): GatewayAdminProxyMarkerVerification {\n const loopback = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_LOOPBACK_HEADER]);\n const timestamp = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_TIMESTAMP_HEADER]);\n const signature = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_SIGNATURE_HEADER]);\n const nonce = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_NONCE_HEADER]);\n const intentHeader = firstHeader(input.headers[GATEWAY_ADMIN_PROXY_INTENT_HEADER]);\n const present = loopback !== undefined || timestamp !== undefined || signature !== undefined;\n if (!present) {\n return { present: false, valid: false, originalClientLoopback: false };\n }\n if (!input.secret) {\n return { present: true, valid: false, originalClientLoopback: false, reason: 'missing_secret' };\n }\n if (loopback !== '0' && loopback !== '1') {\n return { present: true, valid: false, originalClientLoopback: false, reason: 'invalid_loopback' };\n }\n if (!timestamp || !signature) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'incomplete_marker' };\n }\n\n const issuedAt = Number.parseInt(timestamp, 10);\n if (!Number.isFinite(issuedAt)) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'invalid_timestamp' };\n }\n if (Math.abs((input.now ?? Date.now()) - issuedAt) > MAX_CLOCK_SKEW_MS) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'expired_marker' };\n }\n\n const intent = parseGatewayAdminProxyIntent(intentHeader);\n if (intentHeader !== undefined && !intent) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'invalid_intent' };\n }\n\n const expected = signGatewayAdminProxyMarker({\n secret: input.secret,\n method: input.method,\n url: input.url,\n loopback,\n issuedAt,\n nonce,\n intent: intentHeader,\n });\n if (!safeEqual(signature, expected)) {\n return { present: true, valid: false, originalClientLoopback: loopback === '1', reason: 'bad_signature' };\n }\n\n return { present: true, valid: true, originalClientLoopback: loopback === '1', nonce, intent };\n}\n\nfunction signGatewayAdminProxyMarker(input: {\n secret: string;\n method: string | undefined;\n url: string | undefined;\n loopback: '0' | '1';\n issuedAt: number;\n nonce?: string;\n intent?: string;\n}): string {\n const payload = [\n 'v1',\n (input.method ?? 'GET').toUpperCase(),\n input.url ?? '/',\n input.loopback,\n String(input.issuedAt),\n input.nonce ?? '',\n input.intent ?? '',\n ].join('\\n');\n return createHmac('sha256', input.secret).update(payload).digest('base64url');\n}\n\nexport function canonicalGatewayAdminProxyIntent(intent: GatewayAdminProxyIntent): string {\n return JSON.stringify({\n ownerWebId: intent.ownerWebId,\n method: intent.method,\n resourceUrl: intent.resourceUrl,\n principalKind: intent.principalKind,\n scopes: [...intent.scopes].sort(),\n });\n}\n\nfunction parseGatewayAdminProxyIntent(value: string | undefined): GatewayAdminProxyIntent | undefined {\n if (!value) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(value) as Partial<GatewayAdminProxyIntent>;\n if (typeof parsed.ownerWebId !== 'string' ||\n typeof parsed.resourceUrl !== 'string' ||\n (parsed.method !== 'GET' && parsed.method !== 'HEAD' && parsed.method !== 'PUT' && parsed.method !== 'PATCH' && parsed.method !== 'DELETE') ||\n parsed.principalKind !== 'solid-user' ||\n !Array.isArray(parsed.scopes) ||\n !parsed.scopes.every((scope) => typeof scope === 'string')) {\n return undefined;\n }\n return {\n ownerWebId: parsed.ownerWebId,\n method: parsed.method,\n resourceUrl: parsed.resourceUrl,\n principalKind: parsed.principalKind,\n scopes: parsed.scopes,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction safeEqual(actual: string, expected: string): boolean {\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);\n}\n\nfunction firstHeader(value: string | string[] | undefined): string | undefined {\n return Array.isArray(value) ? value[0] : value;\n}\n"]}
@@ -25,6 +25,7 @@ export declare class GatewayProxy {
25
25
  private isApiWebProductPath;
26
26
  private shouldRouteToApi;
27
27
  private applyInternalAdminProxyHeaders;
28
+ private verifiedInternalPodProxyHeaders;
28
29
  private isApiHost;
29
30
  private configuredApiHosts;
30
31
  private hostsFromUrlList;
@@ -106,7 +106,11 @@ class GatewayProxy {
106
106
  const origin = req.headers.origin;
107
107
  const originalRemoteAddress = this.clientRemoteAddressResolver?.(req) ?? req.socket.remoteAddress;
108
108
  const originalClientLoopback = (0, GatewayAdminProxyAuth_1.isLoopbackRemoteAddress)(originalRemoteAddress);
109
+ const internalPodProxyHeaders = this.verifiedInternalPodProxyHeaders(req, originalClientLoopback);
109
110
  (0, GatewayAdminProxyAuth_1.stripGatewayAdminProxyHeaders)(req.headers);
111
+ if (internalPodProxyHeaders) {
112
+ Object.assign(req.headers, internalPodProxyHeaders);
113
+ }
110
114
  // Store public host for routing before any CSS canonical-host rewrites.
111
115
  // External gateways pass the original domain through X-Forwarded-Host;
112
116
  // direct/local requests use Host.
@@ -212,6 +216,24 @@ class GatewayProxy {
212
216
  originalClientLoopback,
213
217
  }));
214
218
  }
219
+ verifiedInternalPodProxyHeaders(req, originalClientLoopback) {
220
+ if (!originalClientLoopback || req.url !== '/.internal/pod-data') {
221
+ return undefined;
222
+ }
223
+ const verification = (0, GatewayAdminProxyAuth_1.verifyGatewayAdminProxyHeaders)({
224
+ headers: req.headers,
225
+ secret: this.internalAdminAuthSecret,
226
+ method: req.method,
227
+ url: req.url,
228
+ });
229
+ if (!verification.valid || !verification.originalClientLoopback || !verification.intent || !verification.nonce) {
230
+ return undefined;
231
+ }
232
+ return Object.fromEntries(GatewayAdminProxyAuth_1.GATEWAY_ADMIN_PROXY_HEADERS.flatMap((header) => {
233
+ const value = req.headers[header];
234
+ return value === undefined ? [] : [[header, value]];
235
+ }));
236
+ }
215
237
  isApiHost(hostHeader) {
216
238
  const host = this.normalizeHost(hostHeader);
217
239
  if (!host) {