@gen3/workspaces 0.12.55 → 0.12.57

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 (130) hide show
  1. package/dist/dts/api/hatcheryApiHandler.d.ts.map +1 -1
  2. package/dist/dts/api/hatcheryApiHandler.js +3 -0
  3. package/dist/dts/api/hatcheryApiHandler.js.map +1 -1
  4. package/dist/dts/api/kernelApiHandler.d.ts.map +1 -1
  5. package/dist/dts/api/kernelApiHandler.js +0 -2
  6. package/dist/dts/api/kernelApiHandler.js.map +1 -1
  7. package/dist/dts/api/workspaceAssetsApi.d.ts +17 -0
  8. package/dist/dts/api/workspaceAssetsApi.d.ts.map +1 -1
  9. package/dist/dts/api/workspaceAssetsApi.js +115 -123
  10. package/dist/dts/api/workspaceAssetsApi.js.map +1 -1
  11. package/dist/dts/auth/auth.js +1 -1
  12. package/dist/dts/auth/auth.js.map +1 -1
  13. package/dist/dts/components/AutoFitIframe.d.ts +1 -1
  14. package/dist/dts/components/AutoFitIframe.d.ts.map +1 -1
  15. package/dist/dts/components/CodingAssistantPanel/CodingAssistantPanel.d.ts +2 -1
  16. package/dist/dts/components/CodingAssistantPanel/CodingAssistantPanel.d.ts.map +1 -1
  17. package/dist/dts/components/CodingAssistantPanel/CodingAssistantPanel.js +0 -1
  18. package/dist/dts/components/CodingAssistantPanel/CodingAssistantPanel.js.map +1 -1
  19. package/dist/dts/components/CodingAssistantPanel/CodingAssistantPanel.stories.d.ts +1 -1
  20. package/dist/dts/components/CodingAssistantPanel/MessageBubble.d.ts +3 -2
  21. package/dist/dts/components/CodingAssistantPanel/MessageBubble.d.ts.map +1 -1
  22. package/dist/dts/components/CodingAssistantPanel/MessageBubble.js +3 -3
  23. package/dist/dts/components/CodingAssistantPanel/MessageBubble.js.map +1 -1
  24. package/dist/dts/components/CodingAssistantPanel/MessageBubble.stories.d.ts +1 -1
  25. package/dist/dts/components/CompactDictionaryPanel/CompactDictionaryPanel.d.ts +2 -1
  26. package/dist/dts/components/CompactDictionaryPanel/CompactDictionaryPanel.d.ts.map +1 -1
  27. package/dist/dts/components/CompactDictionaryPanel/CompactDictionaryPanel.js.map +1 -1
  28. package/dist/dts/components/CompactDictionaryPanel/CompactDictionaryPanel.stories.d.ts +1 -1
  29. package/dist/dts/components/CompactDictionaryPanel/FieldLinkCountBadge.d.ts +2 -1
  30. package/dist/dts/components/CompactDictionaryPanel/FieldLinkCountBadge.d.ts.map +1 -1
  31. package/dist/dts/components/DataToolsPanel.d.ts +2 -1
  32. package/dist/dts/components/DataToolsPanel.d.ts.map +1 -1
  33. package/dist/dts/components/DataToolsPanel.stories.d.ts +1 -1
  34. package/dist/dts/components/DataToolsRail.d.ts +2 -1
  35. package/dist/dts/components/DataToolsRail.d.ts.map +1 -1
  36. package/dist/dts/components/DataToolsRail.stories.d.ts +1 -1
  37. package/dist/dts/components/HorizontalAccordian.d.ts +2 -1
  38. package/dist/dts/components/HorizontalAccordian.d.ts.map +1 -1
  39. package/dist/dts/components/InfrastructureRightPanel.d.ts +2 -1
  40. package/dist/dts/components/InfrastructureRightPanel.d.ts.map +1 -1
  41. package/dist/dts/components/InfrastructureRightPanel.stories.d.ts +1 -1
  42. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelInfoPanel.d.ts +2 -1
  43. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelInfoPanel.d.ts.map +1 -1
  44. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelInfoPanel.js +2 -2
  45. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelInfoPanel.js.map +1 -1
  46. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelsPanel.d.ts +2 -1
  47. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelsPanel.d.ts.map +1 -1
  48. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelsPanel.js +12 -4
  49. package/dist/dts/components/KernelLifecyclePanel/ActiveKernelsPanel.js.map +1 -1
  50. package/dist/dts/components/KernelLifecyclePanel/ConnectionStatusBadge.d.ts +2 -1
  51. package/dist/dts/components/KernelLifecyclePanel/ConnectionStatusBadge.d.ts.map +1 -1
  52. package/dist/dts/components/KernelLifecyclePanel/ConnectionStatusBadge.js +1 -1
  53. package/dist/dts/components/KernelLifecyclePanel/ConnectionStatusBadge.js.map +1 -1
  54. package/dist/dts/components/KernelLifecyclePanel/ConnectionStatusBadge.stories.d.ts +1 -1
  55. package/dist/dts/components/KernelLifecyclePanel/GatewayConnectionPanel.d.ts +2 -1
  56. package/dist/dts/components/KernelLifecyclePanel/GatewayConnectionPanel.d.ts.map +1 -1
  57. package/dist/dts/components/KernelLifecyclePanel/GatewayConnectionPanel.js +5 -1
  58. package/dist/dts/components/KernelLifecyclePanel/GatewayConnectionPanel.js.map +1 -1
  59. package/dist/dts/components/KernelLifecyclePanel/KernelLifecyclePanel.d.ts +2 -1
  60. package/dist/dts/components/KernelLifecyclePanel/KernelLifecyclePanel.d.ts.map +1 -1
  61. package/dist/dts/components/KernelLifecyclePanel/KernelLifecyclePanel.js +2 -2
  62. package/dist/dts/components/KernelLifecyclePanel/KernelLifecyclePanel.js.map +1 -1
  63. package/dist/dts/components/KernelLifecyclePanel/KernelLifecyclePanel.stories.d.ts +1 -1
  64. package/dist/dts/components/KernelLifecyclePanel/KernelSelector.d.ts +2 -1
  65. package/dist/dts/components/KernelLifecyclePanel/KernelSelector.d.ts.map +1 -1
  66. package/dist/dts/components/KernelLifecyclePanel/KernelSelector.js +1 -1
  67. package/dist/dts/components/KernelLifecyclePanel/KernelSelector.js.map +1 -1
  68. package/dist/dts/components/MicroContainerPanel.stories.d.ts +3 -4
  69. package/dist/dts/components/MicroContainerPanel.stories.d.ts.map +1 -1
  70. package/dist/dts/components/MicroContainerReduxPanel.d.ts +2 -1
  71. package/dist/dts/components/MicroContainerReduxPanel.d.ts.map +1 -1
  72. package/dist/dts/components/MicroContainerReduxPanel.js +1 -1
  73. package/dist/dts/components/MicroContainerReduxPanel.js.map +1 -1
  74. package/dist/dts/components/TierSelectorLanding.d.ts +2 -1
  75. package/dist/dts/components/TierSelectorLanding.d.ts.map +1 -1
  76. package/dist/dts/components/TierSelectorLanding.stories.d.ts +1 -1
  77. package/dist/dts/components/UpgradeActionsPanel.d.ts +2 -1
  78. package/dist/dts/components/UpgradeActionsPanel.d.ts.map +1 -1
  79. package/dist/dts/components/UpgradeActionsPanel.stories.d.ts +1 -1
  80. package/dist/dts/components/WorkspaceTierCard.d.ts +2 -1
  81. package/dist/dts/components/WorkspaceTierCard.d.ts.map +1 -1
  82. package/dist/dts/components/WorkspaceTierCard.stories.d.ts +1 -1
  83. package/dist/dts/core/hatcheryApi.js +1 -1
  84. package/dist/dts/core/hatcheryApi.js.map +1 -1
  85. package/dist/dts/providers/JEGWorkspaceProvider.d.ts +2 -2
  86. package/dist/dts/providers/JEGWorkspaceProvider.d.ts.map +1 -1
  87. package/dist/dts/providers/MicroContainerReduxProvider.d.ts +2 -2
  88. package/dist/dts/providers/MicroContainerReduxProvider.d.ts.map +1 -1
  89. package/dist/dts/providers/WorkspacesGatewayProvider.d.ts +1 -1
  90. package/dist/dts/providers/WorkspacesGatewayProvider.d.ts.map +1 -1
  91. package/dist/dts/providers/useMicroContainerRedux.d.ts.map +1 -1
  92. package/dist/dts/providers/useMicroContainerRedux.js +4 -1
  93. package/dist/dts/providers/useMicroContainerRedux.js.map +1 -1
  94. package/dist/dts/server/kernelLifecycleProxy.js +2 -2
  95. package/dist/dts/server/kernelLifecycleProxy.js.map +1 -1
  96. package/dist/dts/server/withJupyterWorkspaces.js +1 -1
  97. package/dist/dts/server/withJupyterWorkspaces.js.map +1 -1
  98. package/dist/dts/server/workspaceAssetsHandler.d.ts +20 -1
  99. package/dist/dts/server/workspaceAssetsHandler.d.ts.map +1 -1
  100. package/dist/dts/server/workspaceAssetsHandler.js +71 -20
  101. package/dist/dts/server/workspaceAssetsHandler.js.map +1 -1
  102. package/dist/dts/workspace/WorkspaceCenter.d.ts +2 -1
  103. package/dist/dts/workspace/WorkspaceCenter.d.ts.map +1 -1
  104. package/dist/dts/workspace/WorkspaceLayout/SettingsPanel.d.ts +2 -1
  105. package/dist/dts/workspace/WorkspaceLayout/SettingsPanel.d.ts.map +1 -1
  106. package/dist/dts/workspace/WorkspaceLayout/ToolsPanel.d.ts +2 -1
  107. package/dist/dts/workspace/WorkspaceLayout/ToolsPanel.d.ts.map +1 -1
  108. package/dist/dts/workspace/WorkspaceLayout/WorkspaceLayout.d.ts +2 -2
  109. package/dist/dts/workspace/WorkspaceLayout/WorkspaceLayout.d.ts.map +1 -1
  110. package/dist/dts/workspace/tiers/FreeWorkspace.d.ts +2 -1
  111. package/dist/dts/workspace/tiers/FreeWorkspace.d.ts.map +1 -1
  112. package/dist/dts/workspace/tiers/FreeWorkspace.js.map +1 -1
  113. package/dist/index/cjs/index.js +41 -34
  114. package/dist/index/cjs/index.js.map +1 -1
  115. package/dist/index/esm/index.js +41 -34
  116. package/dist/index/esm/index.js.map +1 -1
  117. package/dist/jupyterlite-builds/free-private/requirements.txt +1 -1
  118. package/dist/jupyterlite-builds/remote-private/requirements.txt +1 -1
  119. package/dist/server/cjs/server.js +140 -131
  120. package/dist/server/cjs/server.js.map +1 -1
  121. package/dist/server/esm/server.js +140 -131
  122. package/dist/server/esm/server.js.map +1 -1
  123. package/dist/server.d.ts +38 -0
  124. package/jupyterlite-builds/free-private/requirements.txt +1 -1
  125. package/jupyterlite-builds/remote-private/requirements.txt +1 -1
  126. package/package.json +9 -8
  127. package/dist/dts/hooks/useMicroContainer.d.ts +0 -1
  128. package/dist/dts/hooks/useMicroContainer.d.ts.map +0 -1
  129. package/dist/dts/hooks/useMicroContainer.js +0 -319
  130. package/dist/dts/hooks/useMicroContainer.js.map +0 -1
@@ -1,3 +1,3 @@
1
- jupyterlite-core>=0.8.0
1
+ jupyterlite-core>=0.8.1
2
2
  jupyterlab>=4.6.0
3
3
  jupyterlite-pyodide-kernel
@@ -1,4 +1,4 @@
1
- jupyterlite-core>=0.8.0
1
+ jupyterlite-core>=0.8.1
2
2
  jupyterlab>=4.6.0
3
3
  jupyterlite-pyodide-kernel
4
4
  jupyterlite-remote-server>=0.2.0
@@ -37,6 +37,7 @@ const MIME_TYPES = {
37
37
  '.map': 'application/json; charset=utf-8',
38
38
  '.ipynb': 'application/json; charset=utf-8'
39
39
  };
40
+ const DEFAULT_FULL_THEMES_URL = '/workspace-api/workspace-assets/remote/build/themes';
40
41
  const REMOTE_DISABLED_EXTENSIONS = [
41
42
  '@jupyterlite/services-extension:config-section-manager',
42
43
  '@jupyterlite/services-extension:default-drive',
@@ -57,10 +58,26 @@ const ASSETS_ROOT_PATH = process.env.JUPYTER_ASSETS_ROOT_PATH || '/gen3/jupyter-
57
58
  function defaultAssetRoot() {
58
59
  return ASSETS_ROOT_PATH.startsWith('/') ? ASSETS_ROOT_PATH : nodePath.join(process.cwd(), ASSETS_ROOT_PATH);
59
60
  }
61
+ const DEFAULT_REMOTE_BRANDING = {
62
+ appName: 'JupyterLite',
63
+ faviconUrl: '/favicon2.ico'
64
+ };
65
+ /** Escape characters that are special inside an HTML text node / attribute. */ function escapeHtml(text) {
66
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
67
+ }
68
+ /**
69
+ * Replace the document title with the configured page title (or app name).
70
+ * Values originate from a server-side config file, but they are escaped so a
71
+ * stray `<` in a commons name cannot break out of the title element.
72
+ */ function replaceTitle(html, branding) {
73
+ const title = branding.pageTitle ?? branding.appName;
74
+ if (!title) return html;
75
+ return html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
76
+ }
60
77
  /**
61
78
  * Resolve the origin from forwarding headers, with referer fallback.
62
79
  * Handles proxies that drop non-standard ports from the Host header.
63
- */ function resolveOrigin$1(req) {
80
+ */ function resolveOrigin(req) {
64
81
  const forwardedProto = (Array.isArray(req.headers['x-forwarded-proto']) ? req.headers['x-forwarded-proto'][0] : req.headers['x-forwarded-proto']) || 'http';
65
82
  const forwardedHost = Array.isArray(req.headers['x-forwarded-host']) ? req.headers['x-forwarded-host'][0] : req.headers['x-forwarded-host'];
66
83
  const hostHeader = Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host;
@@ -88,11 +105,10 @@ function defaultAssetRoot() {
88
105
  host: resolvedHost
89
106
  };
90
107
  }
91
- function injectRemoteConfig(html, req, gatewayBaseUrl, disabledExtensions, branding = {
92
- appName: 'JupyterLite',
93
- faviconUrl: '/favicon2.ico'
94
- }) {
95
- const { proto, host } = resolveOrigin$1(req);
108
+ function injectRemoteConfig(html, req, config) {
109
+ const { gatewayBaseUrl, disabledExtensions, fullThemesUrl, remoteKernelsPath } = config;
110
+ const branding = config.branding ?? DEFAULT_REMOTE_BRANDING;
111
+ const { proto, host } = resolveOrigin(req);
96
112
  const absoluteGatewayBase = `${proto}://${host}${gatewayBaseUrl}`;
97
113
  // Inject server-side JEG token — overrides any client-supplied auth header.
98
114
  // This ensures calls from JupyterLite's own serverconnection.js are also authenticated.
@@ -106,14 +122,20 @@ function injectRemoteConfig(html, req, gatewayBaseUrl, disabledExtensions, brand
106
122
  remoteBaseUrl: absoluteGatewayBase,
107
123
  remoteToken: accessToken,
108
124
  disabledExtensions,
109
- fullThemesUrl: '/workspace-api/workspace-assets/remote/build/themes',
125
+ fullThemesUrl,
110
126
  // Required so JupyterLab sets window.jupyterapp, which the parent
111
127
  // frame uses for polling readiness and attaching remote kernels.
112
128
  exposeAppInBrowser: 'true'
113
129
  };
130
+ if (remoteKernelsPath) {
131
+ // Absolute so JupyterLite does not resolve kernel ops relative to
132
+ // /workspace-api/workspace-assets/remote/. Routing all kernel traffic
133
+ // through jeg-proxy lets JupyterLite see merged container + JEG kernelspecs.
134
+ configData.remoteKernelsBaseUrl = `${proto}://${host}${remoteKernelsPath}`;
135
+ }
114
136
  if (branding?.appName) configData.appName = branding.appName;
115
137
  if (branding?.faviconUrl) configData.faviconUrl = branding.faviconUrl;
116
- let result = html.replace(/(<script\s+id="jupyter-config-data"[^>]*>)\s*(\{[^]*?\})\s*(<\/script>)/, (_match, openTag, existingJson, closeTag)=>{
138
+ const result = html.replace(/(<script\s+id="jupyter-config-data"[^>]*>)\s*(\{[^]*?\})\s*(<\/script>)/, (_match, openTag, existingJson, closeTag)=>{
117
139
  let existing = {};
118
140
  try {
119
141
  existing = JSON.parse(existingJson);
@@ -135,13 +157,10 @@ function injectRemoteConfig(html, req, gatewayBaseUrl, disabledExtensions, brand
135
157
  }
136
158
  return `${openTag}${JSON.stringify(merged)}${closeTag}`;
137
159
  });
138
- if (branding?.appName) {
139
- result = result.replace(/<title>[^<]*<\/title>/, `<title>${branding.appName}</title>`);
140
- }
141
- return result;
160
+ return replaceTitle(result, branding);
142
161
  }
143
162
  function injectBranding(html, branding) {
144
- let result = html.replace(/(<script\s+id="jupyter-config-data"[^>]*>)\s*(\{[^]*?\})\s*(<\/script>)/, (_match, openTag, existingJson, closeTag)=>{
163
+ const result = html.replace(/(<script\s+id="jupyter-config-data"[^>]*>)\s*(\{[^]*?\})\s*(<\/script>)/, (_match, openTag, existingJson, closeTag)=>{
145
164
  let existing = {};
146
165
  try {
147
166
  existing = JSON.parse(existingJson);
@@ -157,10 +176,7 @@ function injectBranding(html, branding) {
157
176
  if (branding.faviconUrl) existing.faviconUrl = branding.faviconUrl;
158
177
  return `${openTag}${JSON.stringify(existing)}${closeTag}`;
159
178
  });
160
- if (branding.appName) {
161
- result = result.replace(/<title>[^<]*<\/title>/, `<title>${branding.appName}</title>`);
162
- }
163
- return result;
179
+ return replaceTitle(result, branding);
164
180
  }
165
181
  // ---------- factory ----------
166
182
  function createWorkspaceAssetsHandler(options) {
@@ -172,10 +188,18 @@ function createWorkspaceAssetsHandler(options) {
172
188
  ...REMOTE_DISABLED_EXTENSIONS,
173
189
  ...options.additionalDisabledExtensions
174
190
  ] : REMOTE_DISABLED_EXTENSIONS;
175
- const branding = options?.appName || options?.faviconUrl ? {
191
+ const branding = options?.appName || options?.faviconUrl || options?.pageTitle ? {
176
192
  appName: options.appName,
177
- faviconUrl: options.faviconUrl
193
+ faviconUrl: options.faviconUrl,
194
+ pageTitle: options.pageTitle
178
195
  } : undefined;
196
+ const remoteInjectionConfig = {
197
+ gatewayBaseUrl,
198
+ disabledExtensions,
199
+ fullThemesUrl: options?.fullThemesUrl ?? DEFAULT_FULL_THEMES_URL,
200
+ remoteKernelsPath: options?.remoteKernelsPath,
201
+ branding
202
+ };
179
203
  // Build the allowed-file sets once at factory time (not per-request) to
180
204
  // avoid unbounded I/O on every incoming request (CWE-770).
181
205
  const walkDir = (dir, set)=>{
@@ -268,7 +292,7 @@ function createWorkspaceAssetsHandler(options) {
268
292
  res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
269
293
  try {
270
294
  const html = fs.readFileSync(filePath, 'utf-8');
271
- const injected = injectRemoteConfig(html, req, gatewayBaseUrl, disabledExtensions, branding);
295
+ const injected = injectRemoteConfig(html, req, remoteInjectionConfig);
272
296
  res.status(200).send(injected);
273
297
  } catch {
274
298
  res.status(500).end('Internal server error');
@@ -302,7 +326,6 @@ function createWorkspaceAssetsHandler(options) {
302
326
  stream.pipe(res);
303
327
  };
304
328
  }
305
- /** Default handler — for one-liner re-exports */ createWorkspaceAssetsHandler();
306
329
 
307
330
  const DEFAULT_WORKSPACE_ROUTES = [
308
331
  '/Workspaces'
@@ -322,7 +345,7 @@ function withJupyterWorkspaces(nextConfig, options) {
322
345
  webpack (config, ctx) {
323
346
  config.resolve = config.resolve ?? {};
324
347
  config.resolve.alias = {
325
- ...config.resolve.alias ?? {},
348
+ ...config.resolve.alias,
326
349
  react: nodePath.dirname(require.resolve('react/package.json')),
327
350
  'react-dom': nodePath.dirname(require.resolve('react-dom/package.json'))
328
351
  };
@@ -577,7 +600,7 @@ function filterAndAnnotateSpecs(raw, policy) {
577
600
  display_name: displayNames[specName] ?? rawSpec.spec.display_name,
578
601
  // Inject billing metadata into spec.metadata (available to browser)
579
602
  metadata: {
580
- ...rawSpec.spec.metadata ?? {},
603
+ ...rawSpec.spec.metadata,
581
604
  costPerHour: costPerHour[specName] ?? 0,
582
605
  nodeType: nodeType[specName] ?? 'unknown'
583
606
  }
@@ -809,7 +832,7 @@ function resolveKernelRoute(method, actionSegments) {
809
832
  const enrichedBody = {
810
833
  ...launchBody,
811
834
  env: {
812
- ...launchBody.env ?? {},
835
+ ...launchBody.env,
813
836
  GEN3_API_KEY_ID: fenceCreds.key_id,
814
837
  GEN3_API_KEY_SECRET: fenceCreds.api_key,
815
838
  NAMESPACE: process.env.NAMESPACE || 'jupyter-pods',
@@ -1023,125 +1046,112 @@ function createKernelProxyHandler(config) {
1023
1046
  };
1024
1047
  }
1025
1048
 
1026
- const upstreamHandler$2 = createWorkspaceAssetsHandler({
1049
+ const CONFIG_ROOT = process.env.GEN3_FRONTEND_CONFIGURATION_ROOT || './config/';
1050
+ const CONFIG_FILENAME = 'workspaceAssets.json';
1051
+ const DEFAULT_OPTIONS = {
1027
1052
  // Route JupyterLite remote-mode kernel WebSocket traffic through revproxy's
1028
1053
  // /lw-workspace/proxy/ nginx block → ambassador-service (ExternalName → Emissary)
1029
1054
  // → JEG. Next.js API routes cannot handle WebSocket upgrades, so we must
1030
1055
  // bypass them and use the nginx path that has allow_upgrade + long timeouts.
1031
- gatewayBaseUrl: '/lw-workspace/proxy/'
1032
- });
1033
- const VECTIS_WORKSPACE_APP_NAME = 'Vectis Workspaces';
1034
- const VECTIS_FAVICON_URL = '/icons/Vectis_Logo_Colored_LightTheme.svg';
1035
- /** Escape characters that are special inside an HTML text node / attribute. */ function escapeHtml(text) {
1036
- return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
1056
+ gatewayBaseUrl: '/lw-workspace/proxy/',
1057
+ // Route all kernel ops through jeg-proxy so JupyterLite sees merged Python3
1058
+ // (container) + JEG GPU kernelspecs. The proxy routes Python3 kernel
1059
+ // launches/channels to the container; GPU kernel launches are gated (403)
1060
+ // with a message to use the Kernel Panel.
1061
+ remoteKernelsPath: '/lw-workspace/proxy/jeg-proxy',
1062
+ // The remote tier attaches to a real kernel gateway, so JupyterLite's
1063
+ // in-browser Pyodide kernel must not be offered alongside it.
1064
+ additionalDisabledExtensions: [
1065
+ '@jupyterlite/pyodide-kernel-extension:kernel'
1066
+ ],
1067
+ appName: core.GEN3_COMMONS_NAME
1068
+ };
1069
+ const STRING_OPTIONS = [
1070
+ 'gatewayBaseUrl',
1071
+ 'assetRoot',
1072
+ 'appName',
1073
+ 'faviconUrl',
1074
+ 'pageTitle',
1075
+ 'remoteKernelsPath',
1076
+ 'fullThemesUrl'
1077
+ ];
1078
+ function configFilePath() {
1079
+ return nodePath.resolve(process.cwd(), CONFIG_ROOT, core.GEN3_COMMONS_NAME, CONFIG_FILENAME);
1037
1080
  }
1038
1081
  /**
1039
- * Return the first value of a header that Node.js may (in rare edge cases)
1040
- * expose as an array. `host` and `referer` are always strings in practice,
1041
- * but this keeps the helper safe for any header name.
1042
- */ function firstHeader(value) {
1043
- if (Array.isArray(value)) return value[0];
1044
- return value;
1045
- }
1046
- function resolveOrigin(req) {
1047
- const forwardedProto = firstHeader(req.headers['x-forwarded-proto']);
1048
- const forwardedHost = firstHeader(req.headers['x-forwarded-host']);
1049
- // `host` is always a single string per Node.js HTTP spec; firstHeader is
1050
- // used for type-safety only.
1051
- const hostHeader = firstHeader(req.headers.host);
1052
- const refererHeader = firstHeader(req.headers.referer);
1053
- let proto = (forwardedProto ?? '').trim();
1054
- let host = (forwardedHost || hostHeader || '').trim();
1055
- if (refererHeader) {
1056
- try {
1057
- const refererUrl = new URL(refererHeader);
1058
- // Prefer referer origin when the host header dropped a non-default port.
1059
- if (!host || !host.includes(':')) {
1060
- host = refererUrl.host;
1061
- }
1062
- // Only fall back to referer protocol when nothing better is available.
1063
- if (!proto) {
1064
- proto = refererUrl.protocol.replace(':', '');
1065
- }
1066
- } catch {
1067
- // Ignore malformed referer; keep header-derived values.
1082
+ * Pick and type-check the known option keys. Unknown keys are ignored so a
1083
+ * config file can carry extra metadata without reaching the factory,
1084
+ * and a wrong-typed value falls back to its default instead of throwing.
1085
+ */ function parseOptions(raw) {
1086
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
1087
+ console.warn(`[workspace-assets] ${CONFIG_FILENAME} is not a JSON object; using defaults.`);
1088
+ return DEFAULT_OPTIONS;
1089
+ }
1090
+ const source = raw;
1091
+ const parsed = {};
1092
+ for (const key of STRING_OPTIONS){
1093
+ const value = source[key];
1094
+ if (value === undefined) continue;
1095
+ if (typeof value === 'string') {
1096
+ parsed[key] = value;
1097
+ } else {
1098
+ console.warn(`[workspace-assets] ignoring ${key}: expected a string, got ${typeof value}.`);
1068
1099
  }
1069
1100
  }
1101
+ const extensions = source.additionalDisabledExtensions;
1102
+ // oxlint-disable-next-line no-console
1103
+ console.log('addtional disabled extensions', source.additionalDisabledExtensions);
1104
+ if (extensions !== undefined) {
1105
+ if (Array.isArray(extensions) && extensions.every((entry)=>typeof entry === 'string')) {
1106
+ parsed.additionalDisabledExtensions = extensions;
1107
+ } else {
1108
+ console.warn('[workspace-assets] ignoring additionalDisabledExtensions: expected an array of strings.');
1109
+ }
1110
+ }
1111
+ // oxlint-disable-next-line no-console
1112
+ console.log('parsed options', parsed);
1070
1113
  return {
1071
- proto: proto || 'http',
1072
- host: host || 'localhost:30080'
1114
+ ...DEFAULT_OPTIONS,
1115
+ ...parsed
1073
1116
  };
1074
1117
  }
1075
- function injectVectisBranding(html, req) {
1076
- const isRemoteTierRequest = (Array.isArray(req.query.tier) ? req.query.tier[0] : req.query.tier) === 'remote';
1077
- // Resolve origin only when needed (remote tier).
1078
- const absoluteRemoteBaseUrl = isRemoteTierRequest ? (()=>{
1079
- const { proto, host } = resolveOrigin(req);
1080
- return {
1081
- base: `${proto}://${host}/lw-workspace/proxy/`,
1082
- proto,
1083
- host
1084
- };
1085
- })() : null;
1086
- const withConfig = html.replace(/(<script\s+id="jupyter-config-data"[^>]*>)([\s\S]*?)(<\/script>)/, (_match, openTag, rawContent, closeTag)=>{
1087
- // Strip leading/trailing whitespace left by the template.
1088
- const trimmed = rawContent.trim();
1089
- // Verify the captured content looks like a JSON object before parsing.
1090
- if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
1091
- console.error('[vectis-branding] jupyter-config-data content is not a JSON object; skipping merge.');
1092
- return _match;
1093
- }
1094
- let existing = {};
1095
- try {
1096
- existing = JSON.parse(trimmed);
1097
- } catch (err) {
1098
- console.error('[vectis-branding] Failed to parse jupyter-config-data JSON; skipping merge.', err);
1099
- return _match;
1100
- }
1101
- const existingDisabled = Array.isArray(existing.disabledExtensions) ? existing.disabledExtensions : [];
1102
- const remoteDisabledExtensions = isRemoteTierRequest ? [
1103
- ...existingDisabled,
1104
- '@jupyterlite/pyodide-kernel-extension:kernel'
1105
- ] : existingDisabled;
1106
- const merged = {
1107
- ...existing,
1108
- appName: core.GEN3_COMMONS_NAME,
1109
- faviconUrl: VECTIS_FAVICON_URL,
1110
- ...remoteDisabledExtensions.length > 0 ? {
1111
- disabledExtensions: Array.from(new Set(remoteDisabledExtensions))
1112
- } : {},
1113
- ...isRemoteTierRequest && absoluteRemoteBaseUrl ? {
1114
- // Use an absolute URL so JupyterLite does not resolve WS endpoints
1115
- // relative to /workspace-api/workspace-assets/remote/.
1116
- remoteBaseUrl: absoluteRemoteBaseUrl.base,
1117
- // Route all kernel ops through jeg-proxy so JupyterLite sees merged
1118
- // Python3 (container) + JEG GPU kernelspecs. The handler routes
1119
- // Python3 kernel launches/channels to the container; GPU kernel
1120
- // launches are gated (403) with a message to use the Kernel Panel.
1121
- remoteKernelsBaseUrl: `${absoluteRemoteBaseUrl.proto}://${absoluteRemoteBaseUrl.host}/lw-workspace/proxy/jeg-proxy`,
1122
- fullThemesUrl: '/workspace-api/workspace-assets/remote/build/themes'
1123
- } : {}
1124
- };
1125
- return `${openTag}${JSON.stringify(merged)}${closeTag}`;
1126
- });
1127
- // Escape the app name before placing it into the HTML title element.
1128
- return withConfig.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(VECTIS_WORKSPACE_APP_NAME)}</title>`);
1118
+ function loadOptions() {
1119
+ const file = configFilePath();
1120
+ try {
1121
+ return parseOptions(JSON.parse(fs.readFileSync(file, 'utf-8')));
1122
+ } catch (err) {
1123
+ const reason = err instanceof Error ? err.message : String(err);
1124
+ console.warn(`[workspace-assets] could not read ${file} (${reason}); using defaults.`);
1125
+ return DEFAULT_OPTIONS;
1126
+ }
1129
1127
  }
1130
- function isHtmlResponse(res) {
1131
- const contentType = res.getHeader('content-type');
1132
- if (typeof contentType === 'string') return contentType.includes('text/html');
1133
- if (Array.isArray(contentType)) return contentType.some((v)=>v.includes('text/html'));
1134
- return false;
1128
+ let upstreamHandler$2;
1129
+ let loadedConfigMtimeMs;
1130
+ /** mtime of the config file, or undefined when it does not exist. */ function configMtimeMs() {
1131
+ try {
1132
+ return fs.statSync(configFilePath()).mtimeMs;
1133
+ } catch {
1134
+ return undefined;
1135
+ }
1135
1136
  }
1136
- function handler$3(req, res) {
1137
- const originalSend = res.send.bind(res);
1138
- res.send = (body)=>{
1139
- if (typeof body === 'string' && isHtmlResponse(res) && body.includes('id="jupyter-config-data"') && body.includes('<title>')) {
1140
- return originalSend(injectVectisBranding(body, req));
1137
+ function getUpstreamHandler() {
1138
+ if (process.env.NODE_ENV === 'development') {
1139
+ // Rebuild when the config file changes so edits don't need a dev-server
1140
+ // restart. Only the cheap stat runs per request — the expensive asset walk
1141
+ // happens on the first request and then only after a real config change.
1142
+ const mtimeMs = configMtimeMs();
1143
+ if (upstreamHandler$2 && mtimeMs !== loadedConfigMtimeMs) {
1144
+ upstreamHandler$2 = undefined;
1141
1145
  }
1142
- return originalSend(body);
1143
- };
1144
- return upstreamHandler$2(req, res);
1146
+ loadedConfigMtimeMs = mtimeMs;
1147
+ }
1148
+ if (!upstreamHandler$2) {
1149
+ upstreamHandler$2 = createWorkspaceAssetsHandler(loadOptions());
1150
+ }
1151
+ return upstreamHandler$2;
1152
+ }
1153
+ function handler$3(req, res) {
1154
+ return getUpstreamHandler()(req, res);
1145
1155
  }
1146
1156
 
1147
1157
  // Allowed path prefixes — keeps proxying scoped to the Jupyter Gateway API surface
@@ -1325,8 +1335,6 @@ const upstreamHandler$1 = createKernelProxyHandler({
1325
1335
  }
1326
1336
  });
1327
1337
  async function handler$1(req, res) {
1328
- const action = Array.isArray(req.query.action) ? req.query.action : [];
1329
- action[0] === 'api' && action[1] === 'status';
1330
1338
  try {
1331
1339
  await upstreamHandler$1(req, res);
1332
1340
  } catch (err) {
@@ -1492,6 +1500,7 @@ const upstreamHandler = createHatcheryProxyHandler({
1492
1500
  getToken: (req)=>server$1.getAccessToken(req.headers['cookie'] ?? '') ?? null
1493
1501
  });
1494
1502
  async function handler(req, res) {
1503
+ // oxlint-disable-next-line no-unused-vars
1495
1504
  req.query.action?.[0];
1496
1505
  const originalSend = res.send.bind(res);
1497
1506
  res.send = (body)=>{