@gen3/workspaces 0.12.56 → 0.12.58

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