=== modified file 'src/com/goldencode/p2j/main/WebClientSpawner.java'
--- old/src/com/goldencode/p2j/main/WebClientSpawner.java	2026-01-21 08:59:17 +0000
+++ new/src/com/goldencode/p2j/main/WebClientSpawner.java	2026-03-27 07:11:05 +0000
@@ -38,6 +38,7 @@
 ** 020 TJD 20250807 Added SNI_HOST_CHECK option to control Jetty's SNIHostCheck feature
 ** 021 SBI 20251019 Reused ConfigItem.BROKER_DEDICATED_MODE.
 ** 022 TG  20260121 Added support for quoted cfg overrides.
+** 023 SBI 20260325 Provided WebTokensService parameter to startupServer.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -411,6 +412,18 @@
          config.setConfigItem("security", "trust_mgr", "disable", "false");
          config.setConfigItem("security", "transport", "refresh", "true");
 
+         WebTokensService webTokensService = null;
+         try 
+         {
+            // create WebTokenService
+            webTokensService = new WebTokensService();
+         }
+         catch (Exception e1)
+         {
+            LOG.severe("new WebTokensService() failed", e1);
+            throw new RuntimeException(e1);
+         }
+         
          // get an explicit port and host set by the client
          int port = config.getInt(ConfigItem.PORT, 0);
          String host = config.getString(ConfigItem.HOST, null);
@@ -430,7 +443,7 @@
          ScreenDriver<?> driver = gui ? new GuiWebDriver(config) : new ChuiWebDriver(config);
          driver.init();
          
-         String uri = ((EmbeddedWebServer) driver).startupServer(keyStore, config, host, port);
+         String uri = ((EmbeddedWebServer) driver).startupServer(keyStore, config, host, port, webTokensService);
          
          String forwardedHost = config.getString(ConfigItem.PROXY_HOST, null);
          

=== modified file 'src/com/goldencode/p2j/ui/chui/ThinClient.java'
--- old/src/com/goldencode/p2j/ui/chui/ThinClient.java	2026-02-26 11:54:20 +0000
+++ new/src/com/goldencode/p2j/ui/chui/ThinClient.java	2026-03-27 07:12:44 +0000
@@ -3182,6 +3182,7 @@
 **      PMP 20260223          Prevented focus reset also for (forward) TAB. Refs: #11085-8.
 ** 1083 DMM 20260127          Do not prevent MOUSE-PRESSED event for the drop-down of the active combo-box.
 ** 1084 SP  20260226          Added sendSsoReauth() for oidc SSO re-authentication.
+** 1085 SBI 20260325          Provided WebTokensService to web client.
 */
 
 /*
@@ -3262,6 +3263,7 @@
 import com.goldencode.p2j.ui.client.chui.driver.batch.*;
 import com.goldencode.p2j.ui.client.driver.*;
 import com.goldencode.p2j.ui.client.driver.ScreenDriver.*;
+import com.goldencode.p2j.ui.client.driver.web.WebTokensService;
 import com.goldencode.p2j.ui.client.event.*;
 import com.goldencode.p2j.ui.client.event.FocusEvent;
 import com.goldencode.p2j.ui.client.event.InvocationEvent;
@@ -18409,6 +18411,21 @@
    }
 
    /**
+    * Expose the web tokens service for the client usages.
+    * 
+    * @return   The web tokens service.
+    */
+   public WebTokensService getWebTokensService()
+   {
+      ScreenDriver<?> driver = tk.getDriver();
+      if (driver.isWeb())
+      {
+         return ((IWebTokensServiceProvider) driver).getWebTokensService();
+      }
+      return null;
+   }
+
+   /**
     * Get <code>Component</code> instance for a given ID.
     *
     * @param   componentId

=== modified file 'src/com/goldencode/p2j/ui/client/chui/driver/web/ChuiWebDriver.java'
--- old/src/com/goldencode/p2j/ui/client/chui/driver/web/ChuiWebDriver.java	2026-03-19 09:49:21 +0000
+++ new/src/com/goldencode/p2j/ui/client/chui/driver/web/ChuiWebDriver.java	2026-03-25 20:23:26 +0000
@@ -51,6 +51,7 @@
 ** 031 SBI 20250128 Removed usages of getKeyboardLayoutsSettings() as a part of removed code related to
 **                  keyboard layouts implementation.
 ** 032 SP  20260226 Added sendSsoReauth for SSO re-authentication.
+** 033 SBI 20260325 Provided WebTokensService parameter to startupServer.
 */
 
 /*
@@ -137,7 +138,8 @@
 public class ChuiWebDriver
 extends AbstractChuiDriver
 implements EmbeddedWebServer,
-           ScreenWebDriver
+           ScreenWebDriver,
+           IWebTokensServiceProvider
 {
    /** Driver name. */
    public static final String NAME = "web-chui";
@@ -169,6 +171,9 @@
    /** Web-Socket which provides protocol for communication with the browser. */
    private StorageMessagingWebSocket websock;
 
+   /** The web tokens service */
+   private WebTokensService webTokensService;
+
    /**
     * Constructor.
     * 
@@ -533,18 +538,21 @@
     *           The exported server KeyStore.
     * @param    config
     *           Client configuration.
-    * @param    port
-    *           An explicit port to start the web server or 0 to let the OS assign one.
     * @param    host
     *           An explicit host to start the web server or <code>null</code> to use the server's
     *           host.
+    * @param    port
+    *           An explicit port to start the web server or 0 to let the OS assign one.
+    * @param    webTokensService
+    *           The web tokens service
     * 
     * @return   The embedded web server's URI.
     */
    @Override
-   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port)
+   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port, WebTokensService webTokensService)
    {
-      return websrv.startupServer(keyStore, config, host, port);
+      this.webTokensService = webTokensService;
+      return websrv.startupServer(keyStore, config, host, port, webTokensService);
    }
    
    /**
@@ -740,4 +748,15 @@
          simulator.setCursor(col, row);
       }
    }
+
+   /**
+    * Return web tokens service.
+    * 
+    * @return   Web tokens service
+    */
+   @Override
+   public WebTokensService getWebTokensService()
+   {
+      return this.webTokensService;
+   }
 }

=== added file 'src/com/goldencode/p2j/ui/client/driver/IWebTokensServiceProvider.java'
--- old/src/com/goldencode/p2j/ui/client/driver/IWebTokensServiceProvider.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/IWebTokensServiceProvider.java	2026-03-25 20:20:13 +0000
@@ -0,0 +1,75 @@
+/*
+** Module   : IWebTokensServiceProvide.java
+** Abstract : Provides methods to get web tokens service.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
+** 001 SBI 20260122 Created initial version.
+*/
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+** 
+** Additional terms under GNU Affero GPL version 3 section 7:
+** 
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+** 
+**   0. Attribution Requirement.
+** 
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+** 
+**   1. No License To Use Trademarks.
+** 
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+** 
+**   2. No Misrepresentation of Affiliation.
+** 
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+** 
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+** 
+**   3. No Misrepresentation of Source or Origin.
+** 
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+package com.goldencode.p2j.ui.client.driver;
+
+import com.goldencode.p2j.ui.client.driver.web.WebTokensService;
+
+public interface IWebTokensServiceProvider
+{
+   /**
+    * Return web tokens service.
+    * 
+    * @return   Web tokens service
+    */
+   WebTokensService getWebTokensService();
+}

=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/AuthHandler.java'
--- old/src/com/goldencode/p2j/ui/client/driver/web/AuthHandler.java	2025-07-24 12:06:47 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/AuthHandler.java	2026-03-27 07:34:36 +0000
@@ -2,7 +2,7 @@
 ** Module   : AuthHandler.java
 ** Abstract : web authorization
 **
-** Copyright (c) 2019-2025, Golden Code Development Corporation.
+** Copyright (c) 2019-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 HC  20190910 Initial version.
@@ -13,6 +13,9 @@
 ** 004 GBB 20230908 Auth cookie name to contain port to be identifiable by JS.
 ** 005 GBB 20240529 Auth cookie to be secure, same-site and http only.
 ** 006 TJD 20250316 Upgrade to Jetty 12
+** 007 SBI 20260301 Set cross-site partitioned cookie.
+**         20260326 Added /open/resource and /document handlers to provide
+**                  these resource to authorized users.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -70,6 +73,8 @@
 package com.goldencode.p2j.ui.client.driver.web;
 
 import com.goldencode.p2j.main.*;
+import com.goldencode.p2j.ui.client.gui.driver.OpenResourceTemplate;
+import com.goldencode.p2j.ui.client.gui.driver.web.DocumentOutputHandler;
 import com.goldencode.p2j.util.logging.*;
 import org.apache.commons.codec.digest.*;
 import org.eclipse.jetty.http.*;
@@ -93,6 +98,19 @@
    /** Logger. */
    private static final CentralLogger LOG = CentralLogger.get(AuthHandler.class.getName());
 
+   /**
+    * The secure host prefix used with CHIPS. See
+    * https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#cookie_prefixes
+    */
+   private static final String HOST_SECURE_CHIPS_PREFIX = "__Host-";
+
+   /** The local documents handler */
+   private static final String DOCUMENT_HANDLER = "/document/";
+   /** The embedded resource handler */
+   private static final String EMBEDDED_RESOURCE_HANDLER = OpenResourceTemplate.REQUEST;
+   /** API token name*/
+   public static final String API_TOKEN_NAME = "api_token";
+
    /** Authorization cookie name */
    private String authorizationCookieName;
 
@@ -102,17 +120,21 @@
    /** Authorization token */
    private String authorizationToken;
 
+   /** The web tokens service */
+   private final WebTokensService webTokensService;
+
    /**
     * Constructor.
     *
     * @param    authorizationToken
     *           Authorization token.
     */
-   public AuthHandler(String authorizationToken)
+   public AuthHandler(String authorizationToken, WebTokensService webTokensService)
    {
       this.authorizationToken = authorizationToken;
+      this.webTokensService = webTokensService;
    }
-   
+
    /**
     * Handle the request.
     *    
@@ -161,8 +183,8 @@
       {
          return false;
       }
-
-      return (cookieToken == null) ? checkQueryParam(request, response) : checkCookies(request);
+      
+      return (cookieToken == null) ? checkQueryParam(request, response) : checkCookies(request, response);
    }
    
    /**
@@ -174,7 +196,7 @@
     *          Request instance.
     * @param   response
     *          Response instance.
-    *          
+    * 
     * @return  <code>true</code> if authorized <code>false</code> otherwise.
     */
    @SuppressWarnings("deprecation")
@@ -191,13 +213,14 @@
       if (match)
       {
          // set a cookie using a new generated token
-         authorizationCookieName = "Auth-" + authorizationToken + "-" + UUID.randomUUID().toString();
+         authorizationCookieName = HOST_SECURE_CHIPS_PREFIX + "Auth-" + authorizationToken + "-" + UUID.randomUUID().toString();
          cookieToken = DigestUtils.shaHex(UUID.randomUUID().toString());
          HttpCookie cookie = HttpCookie.build(authorizationCookieName, cookieToken).
                                         path("/").
                                         secure(true).
                                         httpOnly(true).
-                                        sameSite(HttpCookie.SameSite.STRICT).
+                                        sameSite(HttpCookie.SameSite.NONE).
+                                        partitioned(true).
                                         build();
          Response.addCookie(response, cookie);
       }
@@ -211,14 +234,16 @@
     * 
     * @param   request
     *          HttpServletRequest instance.
-    *          
+    * @param   response
+    *          Response instance.
+    * 
     * @return  <code>true</code> if authorized <code>false</code> otherwise.
     */
-   private boolean checkCookies(Request request)
+   private boolean checkCookies(Request request, Response response)
    {
       // check browser cookies
       List<HttpCookie> cookies = Request.getCookies(request);
-   
+      
       if (cookies != null)
       {
          // search for authorization cookie
@@ -233,7 +258,28 @@
             }
          }
       }
+      Fields queryParameters = Request.extractQueryParameters(request, StandardCharsets.UTF_8);
+      String target = request.getContext().getPathInContext(request.getHttpURI().getPath());
       
+      String jwe = queryParameters.getValue(API_TOKEN_NAME);
+      if (jwe != null && !jwe.isEmpty())
+      {
+         String subject = null;
+         if (target != null && target.startsWith(DOCUMENT_HANDLER))
+         {
+            subject = DocumentOutputHandler.getUUID(target);
+         }
+         else if (target != null && target.startsWith(EMBEDDED_RESOURCE_HANDLER))
+         {
+            subject = queryParameters.getValue(OpenResourceTemplate.DOCUMENT_PATH);
+         }
+         
+         // validate access rights
+         if (subject != null)
+         {
+            return webTokensService.validateWebTokenValue(jwe, subject, null);
+         }
+      }
       return false;
    }
 }

=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/EmbeddedWebServer.java'
--- old/src/com/goldencode/p2j/ui/client/driver/web/EmbeddedWebServer.java	2025-03-29 20:32:35 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/EmbeddedWebServer.java	2026-03-25 19:45:15 +0000
@@ -2,7 +2,7 @@
 ** Module   : EmbeddedWebServer.java
 ** Abstract : embedded web server contract
 **
-** Copyright (c) 2013-2023, Golden Code Development Corporation.
+** Copyright (c) 2013-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 MAG 20131218 Created initial version.
@@ -13,6 +13,7 @@
 **                  that it has finished initialized.
 ** 006 HC  20190911 Implemented ad-hoc handler registration.
 ** 007 SBI 20230505 Fixed startupServer java doc.
+** 008 SBI 20260325 Provided WebTokensService parameter to startupServer.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -73,7 +74,6 @@
 import com.goldencode.p2j.main.*;
 import com.goldencode.p2j.util.*;
 import org.eclipse.jetty.server.*;
-import org.eclipse.jetty.util.resource.*;
 
 /**
  * Embedded web server API. 
@@ -88,15 +88,17 @@
     *           The exported server KeyStore. 
     * @param    config
     *           Client configuration.
-    * @param    port
-    *           An explicit port to start the web server or 0 to let the OS assign one.
     * @param    host
     *           An explicit host to start the web server or <code>null</code> to use the server's
     *           host.
+    * @param    port
+    *           An explicit port to start the web server or 0 to let the OS assign one.
+    * @param    webTokensService
+    *           The web tokens service
     * 
     * @return   The embedded web server's URI.
     */
-   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port);
+   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port, WebTokensService webTokensService);
 
    /**
     * Shutdown the embedded web server.

=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/EmbeddedWebServerImpl.java'
--- old/src/com/goldencode/p2j/ui/client/driver/web/EmbeddedWebServerImpl.java	2025-05-19 11:40:58 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/EmbeddedWebServerImpl.java	2026-03-25 19:59:43 +0000
@@ -2,7 +2,7 @@
 ** Module   : EmbeddedWebServerImpl.java
 ** Abstract : embedded web server shared implementation
 **
-** Copyright (c) 2013-2025, Golden Code Development Corporation.
+** Copyright (c) 2013-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 GES 20150312 Created initial version by moving code from the ChUI web driver and making it
@@ -26,6 +26,7 @@
 ** 010 TJD 20250314 Migration to Jetty 12
 **     TJD 20250807 Added SNI_HOST_CHECK option to control Jetty's SNIHostCheck feature
 ** 011 DDF 20250519 Use FileExtensions for all file extensions.
+** 012 SBI 20260325 Provided WebTokensService parameter to startupServer.
 */
 
 /*
@@ -175,16 +176,18 @@
     *           The exported server KeyStore. 
     * @param    config
     *           Client configuration.
-    * @param    port
-    *           An explicit port to start the web server or 0 to let the OS assign one.
     * @param    host
     *           An explicit host to start the web server or <code>null</code> to use the server's
     *           host.
+    * @param    port
+    *           An explicit port to start the web server or 0 to let the OS assign one.
+    * @param    webTokensService
+    *           The web tokens service
     * 
     * @return   The embedded web server's URI.
     */
    @Override
-   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port)
+   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port, WebTokensService webTokensService)
    {
       externalHost = host;
       
@@ -201,7 +204,7 @@
          server.setBootstrapConfig(config);
          
          // add authorization handler
-         server.addHandler(new AuthHandler(token));
+         server.addHandler(new AuthHandler(token, webTokensService));
 
          // process each handler
          for (int i = 0; i < hdlrs.length; i++)

=== added file 'src/com/goldencode/p2j/ui/client/driver/web/WebTokensService.java'
--- old/src/com/goldencode/p2j/ui/client/driver/web/WebTokensService.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/WebTokensService.java	2026-03-27 06:48:24 +0000
@@ -0,0 +1,238 @@
+/*
+** Module   : WebTokensService.java
+** Abstract : Provides methods to generate and validate web tokens.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
+** 001 SBI 20260325 Created initial version.
+*/
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+** 
+** Additional terms under GNU Affero GPL version 3 section 7:
+** 
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+** 
+**   0. Attribution Requirement.
+** 
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+** 
+**   1. No License To Use Trademarks.
+** 
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+** 
+**   2. No Misrepresentation of Affiliation.
+** 
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+** 
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+** 
+**   3. No Misrepresentation of Source or Origin.
+** 
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+package com.goldencode.p2j.ui.client.driver.web;
+
+import java.security.SecureRandom;
+import java.util.Date;
+import java.util.Map;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+
+import com.goldencode.p2j.util.logging.CentralLogger;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.InvalidClaimException;
+import io.jsonwebtoken.Jwe;
+import io.jsonwebtoken.Jwts;
+
+/**
+ * The web client web tokens service
+ */
+public class WebTokensService
+{
+   /** The class logger */
+   private static final CentralLogger LOG = CentralLogger.get(WebTokensService.class);
+   /** The encryption algorithm */
+   private static final String ALGORITHM = "AES";
+   /** 
+    * Cypher internal parameter Jwts.ENC.A128GCM for external usages
+    * String CIPHER_SPEC = "AES/GCM/NoPadding";
+    * int GCM_IV_LENGTH = 12; // 96 = 12 * 8 bits
+    * int GCM_TAG_LENGTH = 16; // 128 = 16 * 8 bits, standard default
+    */
+   /** The key length in bits */
+   private static final int AES_KEY_LENGTH = 128; // 128, 192, or 256 bits
+   /** The default validity interval in milliseconds */
+   private static long VALIDITITY_IN_MILLISECONDS = 60000;
+   
+   /** The secret key */
+   private final SecretKey secretKey;
+   
+   /** The token validity interval in milliseconds */
+   private final long validityInterval;
+   
+   /**
+    * Creates new cryptographic secret key for AES.
+    * 
+    * @return    The secret key
+    * 
+    * @throws    Exception
+    *            It can be an invalid parameter exception or a runtime exception thrown during
+    *            the key generation.
+    */
+   public static SecretKey generateKey()
+   throws Exception
+   {
+      KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
+      keyGen.init(AES_KEY_LENGTH, SecureRandom.getInstanceStrong());
+      return keyGen.generateKey();
+   }
+   
+   /**
+    * Create the web tokens service instance.
+    * 
+    * @throws   Exception
+    *           The secret key generation exception
+    */
+   public WebTokensService()
+   throws Exception
+   {
+      this(generateKey(), VALIDITITY_IN_MILLISECONDS);
+   }
+   
+   /**
+    * Create the web tokens service instance for the given secret key and
+    * validity interval.
+    * 
+    * @param    secretKey
+    *           The secret key
+    * @param    validityInterval
+    *           The validity interval
+    */
+   public WebTokensService(SecretKey secretKey, long validityInterval)
+   {
+      this.secretKey = secretKey;
+      this.validityInterval = validityInterval;
+   }
+
+   /**
+    * Validate the encrypted web token.
+    * 
+    * @param    value
+    *           The string value of the encrypted web token
+    * @param    subject
+    *           The value of "sub" key
+    * @param    claims
+    *           The additional values
+    * 
+    * @return   true if the token is valid, otherwise false
+    */
+   public boolean validateWebTokenValue(String value, String subject, Map<String, String> claims)
+   {
+     try
+     {
+         Jwe<Claims> jwe = Jwts.parser()
+                 .requireSubject(subject)
+                 .decryptWith(secretKey)
+                 .build()
+                 .parseEncryptedClaims(value);
+
+         Claims payload = jwe.getPayload();
+         if (claims != null)
+         {
+            for(Map.Entry<String, String> entry : claims.entrySet())
+            {
+               String claimValue = payload.get(entry.getKey(), String.class);
+               if (claimValue == null || !claimValue.equals(entry.getValue()))
+               {
+                  return false;
+               }
+            }
+         }
+         
+         return true;
+     }
+     catch (io.jsonwebtoken.security.SecurityException e1)
+     {
+        LOG.severe("", e1);
+        return false;
+     }
+     catch (ExpiredJwtException e2)
+     {
+         LOG.severe("", e2);
+         return false;
+     }
+     catch(InvalidClaimException e3)
+     {
+        LOG.severe("", e3);
+        return false;
+     }
+     catch (Exception e4)
+     {
+         LOG.severe("", e4);
+         return false;
+     }
+   }
+   
+   /**
+    * Generate the encrypted web token.
+    * 
+    * @param    subject
+    *           The value of "sub" key
+    * @param    claims
+    *           The additional values
+    * 
+    * @return   The string presentation of the encrypted web token
+    * 
+    * @throws   Exception
+    *           The exception during this operation
+    */
+   public String generateWebTokenValue(String subject, Map<String, String> claims)
+   throws Exception
+   {
+      String jwe = Jwts.builder()
+            .header()
+            .and()
+            .subject(subject)
+            .expiration(new Date(System.currentTimeMillis() + validityInterval))
+            .issuedAt(new Date())
+            .claims(claims)
+            // Encrypt using Direct encryption (dir) and AES-128-GCM
+            .encryptWith(secretKey, Jwts.ENC.A128GCM) 
+            .compact();
+      return jwe;
+   }
+}

=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/res/p2j.js'
--- old/src/com/goldencode/p2j/ui/client/driver/web/res/p2j.js	2025-12-10 12:29:35 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/res/p2j.js	2026-03-27 06:50:22 +0000
@@ -2,7 +2,7 @@
 ** Module   : p2j.js
 ** Abstract : top-level web client java script object 
 **
-** Copyright (c) 2014-2025, Golden Code Development Corporation.
+** Copyright (c) 2014-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ------------------------------Description----------------------------------
 ** 001 MAG 20140110 First version.
@@ -72,6 +72,7 @@
 ** 029 PMP 20250916 Initialized networkSocket module. 
 ** 030 SB  20251113 Remove the cursor overlay on mouse clicks as well if there was no previous mouse move.
 ** 031 SBI 20251210 Added repeat key.(refs: #10984, #10982, #10836)
+** 032 SBI 20260326 Refactored to protect openMimeResource access.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -172,11 +173,8 @@
    /** The dialog identifiers */
    const DIALOGS = { OPENPOPUP : "openMimeResource"}
       
-   /** The instance of the unique copy/cut/paste notification dialog. */
-   var dialog;
-   
-   /** The open popup dialog */
-   var openPopupDialog;
+   /** The set of displayed ModalDialogs */
+   var dialogs = new Set();
    
    /** The notification templates to prompt a user to do an action */
    var prompts = {};
@@ -218,18 +216,20 @@
       i18n = window.i18n();
       
       Object.defineProperty(
-            p2j,
+            me,
             "i18n",
             {
                get: function ()
                {
                   return i18n;
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       var lang = cfg["lang"];
       if (lang)
       {
-         p2j.addTranslations(lang, cfg["translations"]);
+         me.addTranslations(lang, cfg["translations"]);
       }
       /**
        * Prevent back navigation.
@@ -261,37 +261,39 @@
       container = document.getElementById(cfg.container) || document.body;
       
       // save off our client mode type
-      p2j.isGui = cfg.isGui;
+      me.isGui = cfg.isGui;
       
       // set the web client embedded mode and define a "get" access only 
       Object.defineProperty(
-                            p2j,
+                            me,
                             "embedded",
                             {
                                get: function ()
                                {
                                   return cfg.embedded;
-                               }
+                               },
+                               enumerable : false,
+                               configurable : false
                             });
       /** the body html css class name to display wait cursor */
-      p2j.waitCursorClass = "wait";
+      me.waitCursorClass = "wait";
       
       /** the body html css class name to display default cursor */
-      p2j.bodyHtmlClass = document.body.className;
+      me.bodyHtmlClass = document.body.className;
       
       /** to display the wait cursor for the document */
-      p2j.displayWaitCursor = function ()
+      me.displayWaitCursor = function ()
       {
-         if (document.body.className === p2j.bodyHtmlClass)
+         if (document.body.className === me.bodyHtmlClass)
          {
-            document.body.className += (" " + p2j.waitCursorClass);
+            document.body.className += (" " + me.waitCursorClass);
          }
       }
       
       /** to restore the cursor for the document */
-      p2j.restoreCursor = function ()
+      me.restoreCursor = function ()
       {
-         document.body.className = p2j.bodyHtmlClass;
+         document.body.className = me.bodyHtmlClass;
       }
 
       /**
@@ -300,7 +302,7 @@
        * @param   cursorStyle
        *          The id of the cursor style.
        */
-      p2j.setGlobalCursor = function(cursorStyle)
+      me.setGlobalCursor = function(cursorStyle)
       {
          var body = document.body;
          var style = p2j.screen.calculateCursorStyle(cursorStyle);
@@ -366,7 +368,7 @@
       /**
        * Remove the globally assigned cursor.
        */
-      p2j.removeGlobalCursor = function()
+      me.removeGlobalCursor = function()
       {
          var body = document.body;
          var bodyOverlay = document.getElementById("body-overlay");
@@ -382,9 +384,9 @@
        * @param    {String} rgb
        *           The CSS color string.
        */
-      p2j.setDefaultDesktopBgcolor = function(rgb)
+      me.setDefaultDesktopBgcolor = function(rgb)
       {
-         p2j.defaultDesktopBgcolor = rgb;
+         me.defaultDesktopBgcolor = rgb;
       };
       
       /**
@@ -393,21 +395,21 @@
        * @param    {String} rgb
        *           The CSS color string.
        */
-      p2j.setDesktopBgColor = function (rgb)
+      me.setDesktopBgColor = function (rgb)
       {
-         if (p2j.embedded)
+         if (me.embedded)
          {
-            if (p2j.defaultDesktopBgcolor)
+            if (me.defaultDesktopBgcolor)
             {
-               rgb = p2j.defaultDesktopBgcolor;
+               rgb = me.defaultDesktopBgcolor;
             }
 
             setStyleForElement(document.body, {background : rgb});
          }
-      }
-      
+      };
+      // initialize constants
       Object.defineProperty(
-            p2j,
+            me,
             "prompts",
             {
                get: function ()
@@ -421,11 +423,13 @@
                      
                      return prompt;
                   }
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       
       Object.defineProperty(
-            p2j,
+            me,
             "hints",
             {
                get: function ()
@@ -439,11 +443,13 @@
                      
                      return hint;
                   }
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       
       Object.defineProperty(
-         p2j,
+         me,
          "commandSupported",
          {
             get: function ()
@@ -452,63 +458,99 @@
                {
                   return queryCommandSupported[cmd];
                }
-            }
+            },
+            enumerable : false,
+            configurable : false
          });
       
       Object.defineProperty(
-            p2j,
+            me,
             "webRoot",
             {
                get: function ()
                {
                   return cfg.webRoot;
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       
       Object.defineProperty(
-            p2j,
+            me,
             "disablePixelManipulation",
             {
                get: function ()
                {
                   return cfg.disablePixelManipulation;
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       
       /** The container for application windows and tool bar */
       Object.defineProperty(
-            p2j,
+            me,
             "container",
             {
                get: function ()
                {
                   return container;
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       
       //Dialog help messages
       const pressEscape = "Press ESCAPE to dismiss this action.";
       Object.defineProperty(
-            p2j,
+            me,
             "pressEscape",
             {
                get: function ()
                {
                   return i18n.gettext(pressEscape);
-               }
+               },
+               enumerable : false,
+               configurable : false
             });
       
       const pressAnyKey = "Press any key to dismiss this action.";
       Object.defineProperty(
-            p2j,
+            me,
             "pressAnyKey",
             {
                get: function ()
                {
                   return i18n.gettext(pressAnyKey);
-               }
-            });
-      
+               },
+               enumerable : false,
+               configurable : false
+            });
+      Object.defineProperty(
+            me,
+            "dialogs",
+            {
+               get: function ()
+               {
+                  return function(id)
+                  {
+                     return DIALOGS[id];
+                  }
+               },
+               enumerable : false,
+               configurable : false
+            });
+      Object.defineProperty(
+            me,
+            "OPENPOPUP",
+            {
+               get: function ()
+               {
+                  return OPENPOPUP;
+               },
+               enumerable : false,
+               configurable : false
+            });
       var progressBarControlFunction;
       dojo.require("dijit.ProgressBar");
       
@@ -516,13 +558,15 @@
       {
          progressBarControlFunction = createProgressBar("webClientLoadingProgressBar", [0, 50, 100]);
          Object.defineProperty(
-               p2j,
+               me,
                "setLoadingCheckpoint",
                {
                   get: function ()
                   {
                      return progressBarControlFunction;
-                  }
+                  },
+                  enumerable : false,
+                  configurable : false
                });
       });
       
@@ -1181,196 +1225,15 @@
    me.getCodePoint = getCodePoint;
    
    /**
-    * Creates and displays the application modal dialog.
-    * 
-    * @param    {String} id
-    *           The dialog unique identifier.
-    * @param    {HtmlElement} container
-    *           The container element.
-    * @param    {String} text
-    *           The dialog content message.
-    * @param    {String} tooltip
-    *           The dialog tooltip message.
-    * @param    {Function} keyDownHandler
-    *           The "keydown" event's handler.
-    * 
-    * @return   The modal dialog.
-    */
-   function createInternalDialog(id, container, text, tooltip, keyDownHandler)
-   {
-      var dlg = new ModalDialog(id, container, text, tooltip, false);
-      
-      if (keyDownHandler)
-      {
-         dlg.addTrigger("keydown", keyDownHandler, true);
-      }
-      
-      return dlg;
-   };
-   
-   /**
     * Tests if the clipboard dialog is active.
     */
    function isDialogDisplayed()
    {
-      return (dialog != null) && (dialog != undefined);
+      return dialogs.size > 0;
    }
    
    me.isDialogDisplayed = isDialogDisplayed;
    
-   /**
-    * Creates and displays the open popup operation dialog.
-    * 
-    * @param    {Function} keyDownHandler
-    *           The "keydown" event's handler.
-    * 
-    * @return   The modal dialog.
-    */
-   function createOpenPopupDialog(keyDownHandler)
-   {
-      if (!openPopupDialog || openPopupDialog.disposed)
-      {
-         openPopupDialog = createInternalDialog(DIALOGS[OPENPOPUP],
-                                                document.body,
-                                                p2j.prompts(OPENPOPUP),
-                                                p2j.hints(OPENPOPUP),
-                                                keyDownHandler);
-      }
-      else
-      {
-         openPopupDialog.addTrigger("keydown", keyDownHandler, true);
-         openPopupDialog.show();
-      }
-      
-      return openPopupDialog;
-   };
-   
-   /**
-    * Returns the reference to the window with the target page if this operation is succedded,
-    * otherwise null.
-    * 
-    * @param    url
-    *           The target page
-    */
-   function openNewWindow(url)
-   {
-      var newWindow;
-      
-      try
-      {
-         newWindow = window.open(url, "_blank");
-      }
-      catch(ex)
-      {
-         console.error(ex);
-      }
-      
-      if (newWindow && !newWindow.closed)
-      {
-         return newWindow;
-      }
-      
-      return null;
-   }
-   
-   /**
-    * Retrieve filename from content-disposition header if there is filename key, otherwise
-    * returns the provided default file name.
-    * 
-    * @param    response
-    *           The response object
-    * @param    defaultFileName
-    *           The provided default file name
-    * 
-    * @returns  The value of filename key, otherwise the provided default file name.
-    */
-   function getFileName(response, defaultFileName)
-   {
-      var contentDisposition = response.headers.get("Content-disposition");
-      
-      if (contentDisposition)
-      {
-         var index = contentDisposition.indexOf("filename=");
-         if (index > 0)
-         {
-            var filename = contentDisposition.substring(index + 9);
-            // remove quotes
-            if (filename.length > 0)
-            {
-               var startQuote = filename.charAt(0);
-               var endQuote = filename.charAt(filename.length - 1);
-               if (startQuote == "\"" &&  endQuote == "\"")
-               {
-                  filename = filename.substring(1, filename.length - 1);
-               }
-               
-               return filename;
-            }
-         }
-      }
-      
-      return defaultFileName;
-   }
-   
-   /**
-    * Opens a target resource given by its url.
-    * 
-    * @param    url
-    *           The target url
-    * @param    mimeType
-    *           The resource mime type
-    * @param    openInNewWindow
-    *           The flag indicating that the document should be opened in new tab or window
-    */
-   function openMimeResource(url, mimeType, openInNewWindow)
-   {
-      if (openInNewWindow)
-      {
-         var newWindow = openNewWindow(url);
-         
-         if (!newWindow)
-         {
-            createOpenPopupDialog(
-                         function (evt)
-                         {
-                            if (evt.keyCode === keys.ENTER)
-                            {
-                               newWindow = openNewWindow(url);
-                               
-                               if (!newWindow)
-                               {
-                                  console.error("Failed to open " + url);
-                               }
-                               
-                               if (openPopupDialog)
-                               {
-                                  openPopupDialog.close();
-                               }
-                            }
-                         });
-         }
-      }
-      else
-      {
-         fetch(url).then(function(response)
-         {
-            if (response.ok)
-            {
-               var filename = getFileName(response, "filename");
-               response.blob().then(blob =>
-               {
-                 saveBlobAs(filename, new File([blob], filename, {type: mimeType}));
-               });
-            }
-            else
-            {
-               console.error("Failed to fetch " + url + " with status code " + response.status);
-            }
-         });
-      }
-   }
-   
-   me.openMimeResource = openMimeResource;
    
    /**
     * Calculates the maximal z-index on the current page.
@@ -1815,60 +1678,84 @@
    
    me.ScreenManager = ScreenManager;
    
-   /**
-    * Export the given content to the file on the client file system.
-    * 
-    * @param    {String} mimeType
-    *           The content mime type
-    * @param    {String} fileName
-    *           The target file name
-    * @param    {Array} content
-    *           The array of strings that represents the exported content.
-    */
-   function saveAs(mimeType, fileName, content)
-   {
-      var blob = new File(content, fileName, {type: mimeType});
-      saveBlobAs(fileName, blob)
-   }
-   
-   /**
-    * Export the content of the given blob to the file on the client file system.
-    * 
-    * @param    {String} fileName
-    *           The target file name
-    * @param    {Blob} blob
-    *           The blob that represents the exported content.
-    */
-   function saveBlobAs(fileName, blob)
-   {
-      var object_url = URL.createObjectURL(blob);
-      var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
-      var can_use_save_link = "download" in save_link;
-      
-      if (can_use_save_link)
-      {
-         save_link.href = object_url;
-         save_link.target = "_blank";
-         save_link.download = fileName;
-         save_link.click();
-      }
-      else
-      {
-         var opened = window.open(object_url, "_blank");
-         if (!opened)
-         {
-            window.location.href = object_url;
-         }
-      }
-      
-      URL.revokeObjectURL(object_url);
-   }
-   
-   /** Export the given content to the file on the client file system. */
-   me.saveAs = saveAs;
-   
-   /** Export the content of the given blob to the file on the client file system. */
-   me.saveBlobAs = saveBlobAs;
+   // Define protected methods 
+   (function()
+   {
+      /**
+       * Export the given content to the file on the client file system.
+       * 
+       * @param    {String} mimeType
+       *           The content mime type
+       * @param    {String} fileName
+       *           The target file name
+       * @param    {Array} content
+       *           The array of strings that represents the exported content.
+       */
+      function saveAs(mimeType, fileName, content)
+      {
+         var blob = new File(content, fileName, {type: mimeType});
+         saveBlobAs(fileName, blob)
+      }
+      
+      /**
+       * Export the content of the given blob to the file on the client file system.
+       * 
+       * @param    {String} fileName
+       *           The target file name
+       * @param    {Blob} blob
+       *           The blob that represents the exported content.
+       */
+      function saveBlobAs(fileName, blob)
+      {
+         var object_url = URL.createObjectURL(blob);
+         var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
+         var can_use_save_link = "download" in save_link;
+         
+         if (can_use_save_link)
+         {
+            save_link.href = object_url;
+            save_link.target = "_blank";
+            save_link.download = fileName;
+            save_link.click();
+         }
+         else
+         {
+            var opened = window.open(object_url, "_blank");
+            if (!opened)
+            {
+               window.location.href = object_url;
+            }
+         }
+         
+         URL.revokeObjectURL(object_url);
+      }
+      
+      /** Export the given content to the file on the client file system. */
+      Object.defineProperty(
+                            me,
+                            "saveAs",
+                            {
+                               get: function ()
+                               {
+                                  return saveAs;
+                               },
+                               enumerable : false,
+                               configurable : false
+                            });
+      
+      /** Export the content of the given blob to the file on the client file system. */
+      Object.defineProperty(
+                            me,
+                            "saveBlobAs",
+                            {
+                               get: function ()
+                               {
+                                  return saveBlobAs;
+                               },
+                               enumerable : false,
+                               configurable : false
+                            });
+   })();
    
    /** Defines chained listeners */
    function ChainedListeners(head, tail)
@@ -2026,6 +1913,7 @@
             changeDisplayStyle(overlay, "block");
             changeVisibilityStyle(dialog, "visible");
             setVisible(true);
+            dialogs.add(dialog);
          }
       }
       
@@ -2038,6 +1926,7 @@
             changeDisplayStyle(overlay, "none");
             changeVisibilityStyle(dialog, "collapse");
             setVisible(false);
+            dialogs.delete(dialog);
             var listener = dialog.listeners.get("onHide");
             if (listener)
             {
@@ -2379,7 +2268,7 @@
             });
             
             dialog.disposed = true;
-            
+            dialogs.delete(dialog);
             if (listener)
             {
                listener();

=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js'
--- old/src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js	2026-03-19 14:32:38 +0000
+++ new/src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js	2026-03-27 06:37:29 +0000
@@ -200,6 +200,7 @@
 **         20260312 Exposed doRedirectToLogoutPage as me.redirectToLogout for use by SSO re-auth module.
 **                  Added me.loginPage and me.logoutPage accessors to expose the configured page URLs.
 ** 080 SB  20260310 When safely closing a websocket use code Normal_Closure.
+** 081 SBI 20260326 Refactored to protect openMimeResource access.
 */
 
 /*
@@ -4408,7 +4409,17 @@
             
             var anchor = message[offset + 2];
             
-            var url = p2j.socket.buildOpenResourceUrl("document", id, mimeType, embedded, local, anchor);
+            offset = offset + 3;
+            var webTokenForEmbedded = null;
+            if (embedded || local)
+            {
+               textLength = me.readInt32BinaryMessage(message, offset);
+               offset = offset + 4;
+               webTokenForEmbedded = me.readStringBinaryMessageByLength(message, offset, textLength);
+               offset = offset + 2 * textLength;
+            }
+            var url = buildOpenResourceUrl("document", id, mimeType, embedded, local,
+                anchor, webTokenForEmbedded);
             
             if (url != null)
             {
@@ -4421,7 +4432,7 @@
                            return element.toLowerCase() == mimeType.toLowerCase();
                         }).length > 0;
                }
-               p2j.openMimeResource(url, mimeType, !fetch);
+               openMimeResource(url, mimeType, !fetch);
             }
             
             break;
@@ -4854,7 +4865,8 @@
     * 
     * @return   The target resource url.
     */
-   me.buildOpenResourceUrl = function(handler, path, mimeType, embedded, local, anchor)
+   function buildOpenResourceUrl(handler, path, mimeType, embedded, local, 
+      anchor, webTokenForEmbedded)
    {
       var url = p2j.webRoot;
       
@@ -4871,7 +4883,11 @@
          
          if (local)
          {
-            url += "&stream=yes";
+            url += "&local=yes";
+         }
+         if (webTokenForEmbedded)
+         {
+            url = url.concat("&", "api_token", "=", encodeURIComponent(webTokenForEmbedded));
          }
       }
       else
@@ -4879,6 +4895,10 @@
          if (local)
          {
             url += "/document/" + path;
+            if (webTokenForEmbedded)
+            {
+               url = url.concat("?", "api_token", "=", encodeURIComponent(webTokenForEmbedded));
+            }
          }
          else if (anchor > 0)
          {
@@ -4896,6 +4916,189 @@
       return url;
    }
 
+   /** The open popup dialog */
+   var openPopupDialog;
+
+   /**
+    * Creates and displays the application modal dialog.
+    * 
+    * @param    {String} id
+    *           The dialog unique identifier.
+    * @param    {HtmlElement} container
+    *           The container element.
+    * @param    {String} text
+    *           The dialog content message.
+    * @param    {String} tooltip
+    *           The dialog tooltip message.
+    * @param    {Function} keyDownHandler
+    *           The "keydown" event's handler.
+    * 
+    * @return   The modal dialog.
+    */
+   function createInternalDialog(id, container, text, tooltip, keyDownHandler)
+   {
+      var dlg = new p2j.ModalDialog(id, container, text, tooltip, false);
+      
+      if (keyDownHandler)
+      {
+         dlg.addTrigger("keydown", keyDownHandler, true);
+      }
+      
+      return dlg;
+   };
+
+   /**
+    * Creates and displays the open popup operation dialog.
+    * 
+    * @param    {Function} keyDownHandler
+    *           The "keydown" event's handler.
+    * 
+    * @return   The modal dialog.
+    */
+   function createOpenPopupDialog(keyDownHandler)
+   {
+      if (!openPopupDialog || openPopupDialog.disposed)
+      {
+         openPopupDialog = createInternalDialog(p2j.dialogs[OPENPOPUP],
+                                                document.body,
+                                                p2j.prompts(OPENPOPUP),
+                                                p2j.hints(OPENPOPUP),
+                                                keyDownHandler);
+      }
+      else
+      {
+         openPopupDialog.addTrigger("keydown", keyDownHandler, true);
+         openPopupDialog.show();
+      }
+      
+      return openPopupDialog;
+   };
+
+   /**
+    * Returns the reference to the window with the target page if this operation is succedded,
+    * otherwise null.
+    * 
+    * @param    url
+    *           The target page
+    */
+   function openNewWindow(url)
+   {
+      var newWindow;
+      
+      try
+      {
+         newWindow = window.open(url, "_blank");
+      }
+      catch(ex)
+      {
+         console.error(ex);
+      }
+      
+      if (newWindow && !newWindow.closed)
+      {
+         return newWindow;
+      }
+      
+      return null;
+   }
+
+   /**
+    * Retrieve filename from content-disposition header if there is filename key, otherwise
+    * returns the provided default file name.
+    * 
+    * @param    response
+    *           The response object
+    * @param    defaultFileName
+    *           The provided default file name
+    * 
+    * @returns  The value of filename key, otherwise the provided default file name.
+    */
+   function getFileName(response, defaultFileName)
+   {
+      var contentDisposition = response.headers.get("Content-disposition");
+      
+      if (contentDisposition)
+      {
+         var index = contentDisposition.indexOf("filename=");
+         if (index > 0)
+         {
+            var filename = contentDisposition.substring(index + 9);
+            // remove quotes
+            if (filename.length > 0)
+            {
+               var startQuote = filename.charAt(0);
+               var endQuote = filename.charAt(filename.length - 1);
+               if (startQuote == "\"" &&  endQuote == "\"")
+               {
+                  filename = filename.substring(1, filename.length - 1);
+               }
+               
+               return filename;
+            }
+         }
+      }
+      
+      return defaultFileName;
+   }
+
+   /**
+    * Opens a target resource given by its url.
+    * 
+    * @param    url
+    *           The target url
+    * @param    mimeType
+    *           The resource mime type
+    * @param    openInNewWindow
+    *           The flag indicating that the document should be opened in new tab or window
+    */
+   function openMimeResource(url, mimeType, openInNewWindow)
+   {
+      if (openInNewWindow)
+      {
+         var newWindow = openNewWindow(url);
+         
+         if (!newWindow)
+         {
+            createOpenPopupDialog(
+                         function (evt)
+                         {
+                            if (evt.keyCode === keys.ENTER)
+                            {
+                               newWindow = openNewWindow(url);
+                               
+                               if (!newWindow)
+                               {
+                                  console.error("Failed to open " + url);
+                               }
+                               
+                               if (openPopupDialog)
+                               {
+                                  openPopupDialog.close();
+                               }
+                            }
+                         });
+         }
+      }
+      else
+      {
+         fetch(url).then(function(response)
+         {
+            if (response.ok)
+            {
+               var filename = getFileName(response, "filename");
+               response.blob().then(blob =>
+               {
+                 p2j.saveBlobAs(filename, new File([blob], filename, {type: mimeType}));
+               });
+            }
+            else
+            {
+               console.error("Failed to fetch " + url + " with status code " + response.status);
+            }
+         });
+      }
+   }
+
    /**
     * It is responsible for executing the provided callback function repeatedly with the given time delay
     * between each call if the elapsed time exceeds its period. The elapsed time is calculated using the

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/OpenResourceTemplate.java'
--- old/src/com/goldencode/p2j/ui/client/gui/driver/OpenResourceTemplate.java	2018-04-10 09:31:13 +0000
+++ new/src/com/goldencode/p2j/ui/client/gui/driver/OpenResourceTemplate.java	2026-03-27 06:44:19 +0000
@@ -2,10 +2,11 @@
 ** Module   : OpenResourceTemplate.java
 ** Abstract : Defines the open resource template constants.
 **
-** Copyright (c) 2018, Golden Code Development Corporation.
+** Copyright (c) 2018-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 SBI 20180410 Initial version.
+** 002 SBI 20260326 Added REQUEST and refactored LOCAL_RESOURCE.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -83,6 +84,10 @@
    /** The document handler */
    public final String DOCUMENT_HANDLER = "handler";
    
-   /** If the value of this parameter is present, then document is streamed. */
-   public final String DOCUMENT_STREAMED = "stream";
+   /** Represents the local resources */
+   public final String LOCAL_RESOURCE = "local";
+   
+   /** The target request */
+   public final String REQUEST   = "/open/resource/";
+
 }

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/open-resource.html'
--- old/src/com/goldencode/p2j/ui/client/gui/driver/open-resource.html	2022-05-27 11:32:03 +0000
+++ new/src/com/goldencode/p2j/ui/client/gui/driver/open-resource.html	2026-03-27 06:18:55 +0000
@@ -2,12 +2,14 @@
 ** Module   : open-resource.html
 ** Abstract : Represents an open resource template.
 **
-** Copyright (c) 2018-2022, Golden Code Development Corporation.
+** Copyright (c) 2018-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 SBI 20180402 Initial version.
 ** 002 SBI 20220321 Added window onload handler and replaced embed with iframe.
 **     SBI 20220527 Fixed iframe element size and its content scrolling.
+** 003 SBI 20260326 Renamed to isLocal reflecting the correct meanings, added
+**                  api_token to authorize to this resource.
 -->
 <!-- 
 ** This program is free software: you can redistribute it and/or modify
@@ -78,16 +80,24 @@
 <script type="text/javascript">
 function onLoad()
 {
-   var config = {'isStreamed' : ${isStreamed} };
+   var config = {"isLocal" : ${isLocal},
+                  "token" : "${api_token}" };
+   var token = config.token != null && config.token.length > 0;
    var el = document.getElementById("embeddedDocument");
-   if (config.isStreamed)
+   var url;
+   if (config.isLocal)
    {
-      el.src = "https://" + window.location.host + "${webRoot}/${documentHandler}/${documentPath}";
+      url = "https://" + window.location.host + "${webRoot}/${documentHandler}/${documentPath}";
+      if (token)
+      {
+         url = url.concat("?", "api_token", "=", encodeURIComponent(config.token));
+      }
    }
    else
    {
-      el.src = "${documentPath}";
+      url = "${documentPath}";
    }
+   el.src = url;
    el.height = window.innerHeight;
    var resizeListener = function()
    {

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/DocumentOutputHandler.java'
--- old/src/com/goldencode/p2j/ui/client/gui/driver/web/DocumentOutputHandler.java	2025-07-24 12:06:47 +0000
+++ new/src/com/goldencode/p2j/ui/client/gui/driver/web/DocumentOutputHandler.java	2026-03-27 07:39:09 +0000
@@ -2,7 +2,7 @@
 ** Module   : DocumentOutputHandler.java
 ** Abstract : Document output handler.
 **
-** Copyright (c) 2017-2025, Golden Code Development Corporation.
+** Copyright (c) 2017-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 HC  20171024 Initial version.
@@ -14,6 +14,7 @@
 ** 005 SBI 20230106 Changed to be able parse two types of requests for /document/uuid/real-file-name-with-extension
 **                  or /document/uuid
 ** 006 TJD 20250314 Migration to Jetty 12
+** 007 SBI 20260326 Generalized the code logic to getUUID static method.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -96,6 +97,27 @@
    private DocumentOutputStorage storage;
 
    /**
+    * Get the document uuid.
+    * 
+    * @param    target
+    *           The request path
+    * 
+    * @return   The document uuid string
+    */
+   public static String getUUID(String target)
+   {
+      Matcher m = TARGET_REGEXP.matcher(target);
+      if (!m.matches())
+      {
+         return null;
+      }
+
+      String id = m.group(1);
+      String[] parts = id.split("/");
+      
+      return parts[0];
+   }
+   /**
     * Ctor.
     *
     * @param   storage
@@ -126,18 +148,14 @@
    {
       String target = request.getContext().getPathInContext(request.getHttpURI().getPath());
       
-      Matcher m = TARGET_REGEXP.matcher(target);
-      if (!m.matches())
-      {
-         return false;
-      }
-
-      String id = m.group(1);
-      String[] parts = id.split("/");
       UUID uuid;
       try
       {
-         uuid = UUID.fromString(parts[0]);
+         uuid = UUID.fromString(getUUID(target));
+         if (uuid == null)
+         {
+            return false;
+         }
       }
       catch(IllegalArgumentException ex)
       {

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebDriver.java'
--- old/src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebDriver.java	2026-03-13 09:16:03 +0000
+++ new/src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebDriver.java	2026-03-25 20:25:36 +0000
@@ -282,6 +282,7 @@
 ** 107 TJD 20250314 Migration to Jetty 12
 ** 108 DDF 20250519 Use FileExtensions for all file extensions.
 ** 109 SP  20260226 Added sendSsoReauth for SSO re-authentication.
+** 110 SBI 20260325 Provided WebTokensService parameter to startupServer.
 */
 
 /*
@@ -399,7 +400,8 @@
            EmbeddedClient,
            DocumentOutputStorage,
            FileUploadStorage,
-           WebConfigurationConstants
+           WebConfigurationConstants,
+           IWebTokensServiceProvider
 {
    /** Driver name. */
    public static final String NAME = "web-gui";
@@ -557,6 +559,9 @@
    /** The focused widget saved during the last processing of interactive widgets  */
    private int lastFocusedWidget;
 
+   /** The web tokens service */
+   private WebTokensService webTokensService;
+
    /**
     * Constructor.
     * 
@@ -1560,18 +1565,21 @@
     *           The exported server KeyStore. 
     * @param    config
     *           Client configuration.
-    * @param    port
-    *           An explicit port to start the web server or 0 to let the OS assign one.
     * @param    host
     *           An explicit host to start the web server or <code>null</code> to use the server's
     *           host.
+    * @param    port
+    *           An explicit port to start the web server or 0 to let the OS assign one.
+    * @param    webTokensService
+    *           The web tokens service
     * 
     * @return   The embedded web server's URI.
     */
    @Override
-   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port)
+   public String startupServer(ServerKeyStore keyStore, BootstrapConfig config, String host, int port, WebTokensService webTokensService)
    {
-      return websrv.startupServer(keyStore, config, host, port);
+      this.webTokensService = webTokensService;
+      return websrv.startupServer(keyStore, config, host, port, webTokensService);
    }
    
    /**
@@ -4391,5 +4399,15 @@
       
       websock.setWindowOwner(childId, parentId);
    }
-   
+
+   /**
+    * Return web tokens service.
+    * 
+    * @return   Web tokens service
+    */
+   @Override
+   public WebTokensService getWebTokensService()
+   {
+      return this.webTokensService;
+   }
 }

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java'
--- old/src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java	2026-02-25 14:25:47 +0000
+++ new/src/com/goldencode/p2j/ui/client/gui/driver/web/GuiWebSocket.java	2026-03-27 07:09:26 +0000
@@ -187,6 +187,7 @@
 **     IMS 20250527 The propagateWindowState() construct the message with window state and send it to the JS web client.
 **     IMS 20250610 Added a window State into message package from setIconificationState() method.
 ** 072 SB  20260225 Added disablePixelManipulation overload for drawRoundRect.
+** 073 SBI 20260325 Provided web token with openMimeResource message to access the target resource.
 */
 
 /*
@@ -5098,7 +5099,23 @@
                                 boolean local,
                                 UriPathAnchor uriPathAnchor)
    {
-      int len = 1 + 4 + 2 * id.length() + 4 + 2 * mimeType.length() + 3;
+      String webTokenForEmbedded = null;
+      if (embedded || local)
+      {
+         try
+         {
+            WebTokensService webTokensService = ThinClient.getInstance().getWebTokensService();
+            
+            webTokenForEmbedded = webTokensService.generateWebTokenValue(id, null);
+         }
+         catch (Exception e)
+         {
+            LOG.severe("openMimeResource failed!", e);
+            return;
+         }
+      }
+      int len = 1 + 4 + 2 * id.length() + 4 + 2 * mimeType.length() + 3 + 
+            (((embedded || local) && (webTokenForEmbedded != null))? (4 + 2 * webTokenForEmbedded.length()) : 0);
       BinaryMessage msg = new BinaryMessage(MSG_OPEN_MIME_RESOURCE, len);
       msg.writeInt32(id.length());
       msg.writeText(id);
@@ -5107,6 +5124,12 @@
       msg.writeByte((byte) (embedded ? 1 : 0));
       msg.writeByte((byte) (local ? 1 : 0));
       msg.writeByte((byte) uriPathAnchor.ordinal());
+      if ((embedded || local) && (webTokenForEmbedded != null))
+      {
+         msg.writeInt32(webTokenForEmbedded.length());
+         msg.writeText(webTokenForEmbedded);
+      }
+      
       msg.send();
    }
    

=== modified file 'src/com/goldencode/p2j/ui/client/gui/driver/web/OpenResourceHandler.java'
--- old/src/com/goldencode/p2j/ui/client/gui/driver/web/OpenResourceHandler.java	2025-03-29 20:32:35 +0000
+++ new/src/com/goldencode/p2j/ui/client/gui/driver/web/OpenResourceHandler.java	2026-03-27 06:35:50 +0000
@@ -2,12 +2,14 @@
 ** Module   : OpenResourceHandler.java
 ** Abstract : Open resource handler is responsible to deliver target resources to clients.
 **
-** Copyright (c) 2018-2025, Golden Code Development Corporation.
+** Copyright (c) 2018-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 SBI 20180402 Initial version.
 ** 002 GBB 20240709 Hard-coded config name replaced by ConfigItem constant. Syntax simplified.
 ** 003 TJD 20250314 Migration to Jetty 12
+** 004 SBI 20260326 Renamed to isLocal reflecting the correct meanings, added
+**                  api_token to authorize to this resource.
 */
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -65,16 +67,17 @@
 package com.goldencode.p2j.ui.client.gui.driver.web;
 
 
-import java.net.*;
+import java.nio.charset.*;
 import java.util.*;
 import java.util.function.*;
 
 import com.goldencode.p2j.util.*;
 import com.goldencode.util.*;
 import org.eclipse.jetty.server.*;
+import org.eclipse.jetty.util.*;
 
 import com.goldencode.p2j.cfg.*;
-import com.goldencode.p2j.ui.client.gui.driver.OpenResourceTemplate;
+import com.goldencode.p2j.ui.client.gui.driver.*;
 import com.goldencode.p2j.web.*;
 
 
@@ -85,9 +88,6 @@
 extends HtmlResourceHandler
 implements OpenResourceTemplate
 {
-   /** The target request */
-   private static final String REQUEST   = "/open/resource/";
-   
    /** Configuration. */
    private BootstrapConfig config;
    
@@ -109,15 +109,15 @@
     * Generates a map of template keys with their value suppliers in order to fill gaps in
     * the html skeleton template page.
     * 
-    * @param    base
+    * @param    request
     *           The request
     * 
     * @return   The target map of template keys with their value suppliers
     */
    @Override
-   protected Map<String, Supplier<String>> getTemplateKeys(Request base)
+   protected Map<String, Supplier<String>> getTemplateKeys(Request request)
    {
-      OpenResourceKeysProvider provider = new OpenResourceKeysProvider(base);
+      OpenResourceKeysProvider provider = new OpenResourceKeysProvider(request);
       
       return provider.getTemplateKeys();
    }
@@ -132,16 +132,18 @@
       /**
        * Creates this key and value provider.
        * 
-       * @param    base
+       * @param    request
        *           The request
        */
-      public OpenResourceKeysProvider(Request base)
+      public OpenResourceKeysProvider(Request request)
       {
-         super(base);
+         super(request);
          
-         add("isStreamed", () -> {
+         Fields queryParameters = Request.extractQueryParameters(getRequest(), StandardCharsets.UTF_8);
+
+         add("isLocal", () -> {
             try {
-               return StringHelper.hasContent(Request.getParameters(getRequest()).getValue(DOCUMENT_STREAMED)) ?
+               return StringHelper.hasContent(Request.getParameters(getRequest()).getValue(LOCAL_RESOURCE)) ?
                       "true" :
                       "false";
             }
@@ -152,10 +154,11 @@
             }
          });
          add("webRoot", () -> config.getString(ConfigItem.WEB_ROOT, ""));
-         add("documentTitle", () -> decodeUrlEncodedParameter(DOCUMENT_TITLE, ""));
-         add("documentHandler", () -> decodeUrlEncodedParameter(DOCUMENT_HANDLER, "document"));
-         add("documentPath", () -> decodeUrlEncodedParameter(DOCUMENT_PATH, ""));
-         add("documentType", () -> decodeUrlEncodedParameter(DOCUMENT_TYPE, "application/octet-stream"));
+         add("documentTitle", () -> decodeUrlEncodedParameter(queryParameters, DOCUMENT_TITLE, ""));
+         add("documentHandler", () -> decodeUrlEncodedParameter(queryParameters, DOCUMENT_HANDLER, "document"));
+         add("documentPath", () -> decodeUrlEncodedParameter(queryParameters, DOCUMENT_PATH, ""));
+         add("documentType", () -> decodeUrlEncodedParameter(queryParameters, DOCUMENT_TYPE, "application/octet-stream"));
+         add("api_token", () -> decodeUrlEncodedParameter(queryParameters, "api_token", ""));
 
       }
       
@@ -171,32 +174,15 @@
        * 
        * @return   The decoded parameter value if this decoding operation is succeeded
        */
-      private String decodeUrlEncodedParameter(String name, String defValue)
+      private String decodeUrlEncodedParameter(Fields queryParameters, String name, String defValue)
       {
-         String value;
-         try 
-         {
-            value = Request.getParameters(getRequest()).getValue(name);
-         }
-         catch (Exception e) 
-         {
-            value = null;
-         }
+         String value = queryParameters.getValue(name);
          
          if (value == null)
          {
             return defValue;
          }
          
-         try
-         {
-            value = URLDecoder.decode(value, "UTF-8");
-         }
-         catch (Exception ex)
-         {
-            // catch silently
-         }
-         
          return value;
       }
    }

=== modified file 'tools/docker/dbash.sh'
--- old/tools/docker/dbash.sh	2026-02-04 23:02:57 +0000
+++ new/tools/docker/dbash.sh	2026-03-23 14:28:10 +0000
@@ -5,7 +5,7 @@
 function usage()
 {
    cat <<EOF
-Usage: $0 [-dlw] [-v <volume>] [--no-gui] [--no-debug] [--no-jmx] [--pg<14|15|16|17>] [--pg_port=<port>] [--host] [--manual_port=<hport:cport>] [--startup_hook=<startup_file>] [--jdk21] [--app=<appdir>] [--fwd_lib=<fwd_lib>] [-c <cmd_line>] [-f [tag]]
+Usage: $0 [-dlw] [-v <volume>] [--no-gui] [--no-debug] [--no-jmx] [--pg<14|15|16|17>] [--pg_port=<port>] [--host] [--manual_port=<hport:cport>] [--startup_hook=<startup_file>] [--jdk21] [--detach] [--app=<appdir>] [--fwd_lib=<fwd_lib>] [-c <cmd_line>] [-f [tag]]
 
 Create a container based off the ${docker_image} or ${fwddocker_image} Docker image
 
@@ -26,6 +26,7 @@
        --jdk21 = Include JDK21 in the container. The default is JDK17.
        --app=<appdir> = Set /opt/app directory in container to give <appdir> directory on host, typically the deploy directory.
        --fwd_lib=<fwd_lib> = Set FWD_LIB environment variable to <fwd_lib> path. This allows scripts to find FWD.
+       --detach = Run the container detached. Use "docker attach ..." to attach and "CTRL-p CTRL-q" to detach again.
        -c <cmd_line> = Pass a command line to the Docker container.
        -f [tag] = Include FWD in the container. Specify a tag, if desired. (default=${tag}). *MUST be last parameter*
 
@@ -89,6 +90,7 @@
 docker_image="basedev_ubuntu_24.04"
 fwddocker_image="fwddev_4.0_ubuntu_24.04"
 hook_default="./docker/startup_hook.sh"
+run_detached=""
 
 while getopts ":h?dlwv:c:-:f" opt; do
    case $opt in
@@ -119,6 +121,7 @@
              "jdk"* ) jdk_version="${OPTARG}" ;;
              "app"*) appdir=$(echo $OPTARG | cut -d"=" -f2) ;;
              "fwd_lib"*) fwd_lib=$(echo $OPTARG | cut -d"=" -f2) ;;
+             "detach" ) run_detached="--detach" ;;
              "help" ) usage
                       exit 1 ;;
               * ) echo "Unknown option: --${OPTARG}"
@@ -229,7 +232,7 @@
 # Check on startup hook
 [ -z "$hook_file" ] && hook_file=$hook_default
 [ -f "$hook_file" ] && startup_hook="-v ${hook_file}:/docker_install_hook.sh:ro"
-dcmd="$mycmd docker run -e DBASH=true $host_network --name $container_name -h $container_name --rm -it $dbvol $volumes --pull $pull $startup_hook $ports $jdk_env $app $fwd_env $pgdata $env ${repo}${docker_image}:${tag} ${user} -- $cmd_line"
+dcmd="$mycmd docker run $run_detached -e DBASH=true $host_network --name $container_name -h $container_name --rm -it $dbvol $volumes --pull $pull $startup_hook $ports $jdk_env $app $fwd_env $pgdata $env ${repo}${docker_image}:${tag} ${user} -- $cmd_line"
 echo "Running: $dcmd"
 eval $dcmd
 

=== modified file 'tools/docker/docker_build.sh'
--- old/tools/docker/docker_build.sh	2026-02-12 15:52:34 +0000
+++ new/tools/docker/docker_build.sh	2026-03-23 14:28:10 +0000
@@ -4,28 +4,26 @@
 
 function show_usage()
 {
-   i=0;       usage[$i]="\nUsage: $0 [-d <dist>] [-pt] [--latest] \
-[--target_repo=<target_repo>] \
-[--from_repo=<from_repo>] \
-[--from_tag=<from_tag>] \
-[--progress_plain] [--no_cache]"
-   i=$((i+1));usage[$i]="\nBuild the FWD Docker runtime images from the dist images. Tag appropriately, and optionally push."
-   i=$((i+1));usage[$i]="\nWhere:"
-   i=$((i+1));usage[$i]="\t-d <dist> = location of distribution images (default=${dist})"
-   i=$((i+1));usage[$i]="\t-p push image to repository"
-   i=$((i+1));usage[$i]="\t-t just display commands (test)"
-   i=$((i+1));usage[$i]="\t--pg=<pgver> Include PostgreSQL client in image. Build PostgreSQL server images (full and PostgreSQL-only). (<pgver> from ${pgver_low}–${pgver_high})"
-   i=$((i+1));usage[$i]="\t--latest tag as 'latest' as well"
-   i=$((i+1));usage[$i]="\t--target_repo=<target_repo> if pushing, push built images to specified repository (default=${target_repo})"
-   i=$((i+1));usage[$i]="\t--from_repo=<from_repo> specify a repo to pull base images from (including build) ('.' indicates local) (default=${from_repo})"
-   i=$((i+1));usage[$i]="\t--from_tag=<from_tag> specify a tagged base images to use (default=${from_tag})"
-   i=$((i+1));usage[$i]="\t--progress_plain Don't use buildkit so docker build has more details."
-   i=$((i+1));usage[$i]="\t--no_cache Don't use any cached layers."
-   i=$((i+1));usage[$i]="\n -? or -h or --help = show usage."
-
-   for i in "${usage[@]}"; do
-      echo -e $i
-   done
+   cat <<EOF
+
+Usage: $(basename "$0") [-d <dist>] [-pt] [--latest] [--target_repo=<target_repo>] [--from_repo=<from_repo>] [--from_tag=<from_tag>] [--progress_plain] [--no_cache]
+
+Build the FWD Docker runtime images from the dist images. Tag appropriately, and optionally push.
+
+Where:
+   -d <dist> = location of distribution images (default=${dist})
+   -p push image to repository
+   -t just display commands (test)
+   --pg=<pgver> Include PostgreSQL client in image. Build PostgreSQL server images (full and PostgreSQL-only). (<pgver> from ${pgver_low}–${pgver_high})
+   --latest tag as 'latest' as well
+   --target_repo=<target_repo> if pushing, push built images to specified repository (default=${target_repo})
+   --from_repo=<from_repo> specify a repo to pull base images from (including build) ('.' indicates local) (default=${from_repo})
+   --from_tag=<from_tag> specify a tagged base images to use (default=${from_tag})
+   --progress_plain Don't use buildkit so docker build has more details.
+   --no_cache Don't use any cached layers.
+
+   -? or -h or --help = show usage.
+EOF
 }
 
 function create_version_properties()
@@ -68,10 +66,15 @@
 function get_jdk_version()
 {
    local dist=$1
+   local tool="$2"
    local out
 
-   out=$(dbash.sh -c "fwd_jdk_query.sh -z $dist --decorate" | tail -n 1 | tr -d '\r')
-   jdk=$(printf '%s' "$out" | sed -n 's/^jdk=\([0-9]\+\)$/\1/p')
+   out=$(eval $tool | tail -n 1 | tr -d '\r')
+   if [[ "$out" == *"unknown" ]]; then
+      jdk="unknown"
+   else
+      jdk=$(printf '%s' "$out" | sed -n 's/^jdk=\([0-9]\+\)$/\1/p')
+   fi
    echo $jdk
 }
 
@@ -189,10 +192,9 @@
 shift $(($OPTIND - 1))
 
 # Check for crucial helpers
-command -v dbash.sh >/dev/null 2>&1 || {
-   echo "ERROR: dbash.sh not found in PATH" >&2
-   exit 1
-}
+dbash=false; fwdjdktool=false
+$(command -v dbash.sh >/dev/null 2>&1) && dbash=true
+$(command -v ./scripts/fwd_jdk_query.sh >/dev/null 2>&1) && fwdjdktool=true
 
 # Find the dist filenames
 cvtfile=$(ls "$dist"/fwd_deploy-convert_4.0.0_p2j_*.zip 2>/dev/null | head -n 1)
@@ -207,15 +209,23 @@
 revision=${revision%%_*}
 
 # Validate JDK
-jdk_version=$(get_jdk_version $cvtfile)
-if [[ "$jdk_version" != "17" ]] && [[ "$jdk_version" != "21" ]]; then
+if [[ $fwdjdktool == true ]]; then
+   jdk_version=$(get_jdk_version $cvtfile "./scripts/fwd_jdk_query.sh -z ${cvtfile} --decorate")
+elif [[ $dbash == true ]]; then
+   jdk_version=$(get_jdk_version $cvtfile "dbash.sh -c \"fwd_jdk_query.sh -z ${cvtfile} --decorate\"")
+fi
+if [[ "$jdk_version" == *"unknown"* ]]; then
+   echo "WARNING: Unable to verify JDK used to build $dist via dbash.sh or ./scripts/fwd_jdk_query.sh. Assuming 17." >&2
+   jdk_version=17
+fi
+if [[ "$jdk_version" != "17" && "$jdk_version" != "21" ]]; then
    echo "Error: Invalid jdk_version found: ${jdk_version}. Valid choices are \"17\" or \"21\""
    exit 1
 fi
 echo "Found branch=$branch, revision=$revision, converted with JDK${jdk_version}"
 
 # Valide PG, if any
-if [ "$include_pg" == true ]; then
+if [[ "$include_pg" == true ]]; then
    # Must be within supported range
    if (( pgver < pgver_low || pgver > pgver_high )); then
       echo "ERROR: PostgreSQL version '$pgver' is not supported." >&2
@@ -225,9 +235,8 @@
 fi
 
 # Setup latest tag name, if branch is not trunk
-if [[ "$tag_latest" == "true" ]] && [[ "$branch" != "trunk" ]]; then
-   latest_tag="${branch}_latest"
-fi
+[[ "$branch" != "trunk" ]] && latest_tag="${branch}_latest"
+
 # Setup tagging default
 image_tag="${branch}_${revision}"
 
@@ -248,24 +257,25 @@
 
 # Build the basic runtime image. Make sure PG client is included, if requested.
 build_args=()
-if [ "$include_pg" == true ]; then
+if [[ "$include_pg" == true ]]; then
    build_args+=(
        --build-arg "INSTALL_PG_CLIENT=true"
        --build-arg "PG_VERSION=${pgver}"
    )
 fi
 docker_build_image ${from_repo}${base_image}:${from_tag} ${fwdrt_image_name}:${image_tag} "convert spawner" "${build_args[@]}"
-[ "$tag_latest" == true ] && tag_image ${fwdrt_image_name}:${image_tag} ${fwdrt_image_name}:${latest_tag}
-[ "$push_image" ==  true ] && tag_image ${fwdrt_image_name}:${latest_tag} ${target_repo}/${fwdrt_image_name}:${latest_tag}
+[[ "$tag_latest" == true ]] && tag_image ${fwdrt_image_name}:${image_tag} ${fwdrt_image_name}:${latest_tag}
+[[ "$push_image" ==  true ]] && tag_image ${fwdrt_image_name}:${image_tag} ${target_repo}/${fwdrt_image_name}:${image_tag} && \
+                                tag_image ${fwdrt_image_name}:${latest_tag} ${target_repo}/${fwdrt_image_name}:${latest_tag}
 # Now push, if requested.
-if [ "$push_image" ==  true ]; then
+if [[ "$push_image" ==  true ]]; then
    push_image ${target_repo}/${fwdrt_image_name}:${image_tag}
    # Latest tag is handled separately.
    [ "$tag_latest" == true ] && push_image ${target_repo}/${fwdrt_image_name}:${latest_tag}
 fi
 
 # Build PG database versions, if requested.
-if [ "$include_pg" == true ]; then
+if [[ "$include_pg" == true ]]; then
 
    # We don't need to include the PG client, since the base image we are using include PG server.
    build_args=()
@@ -273,12 +283,13 @@
    pgbase_image=$(printf "%s_pg%d" $base_image $pgver)
    fwdpgrt_image_name=$(printf "%s_pg%d_jdk%d" $fwd_image $pgver $jdk_version)
    docker_build_image ${from_repo}${pgbase_image}:${from_tag} ${fwdpgrt_image_name}:${image_tag} "convert spawner" "${build_args[@]}"
-   [ "$tag_latest" == true ] && tag_image ${fwdpgrt_image_name}:${image_tag} ${fwdpgrt_image_name}:${latest_tag}
-   [ "$push_image" ==  true ] && tag_image ${fwdpgrt_image_name}:${latest_tag} ${target_repo}/${fwdpgrt_image_name}:${latest_tag}
-   if [ "$push_image" ==  true ]; then
+   [[ "$tag_latest" == true ]] && tag_image ${fwdpgrt_image_name}:${image_tag} ${fwdpgrt_image_name}:${latest_tag}
+   [[ "$push_image" ==  true ]] && tag_image ${fwdpgrt_image_name}:${image_tag} ${target_repo}/${fwdpgrt_image_name}:${image_tag} && \
+                                   tag_image ${fwdpgrt_image_name}:${latest_tag} ${target_repo}/${fwdpgrt_image_name}:${latest_tag}
+   if [[ "$push_image" ==  true ]]; then
       push_image ${target_repo}/${fwdpgrt_image_name}:${image_tag}
       # Latest tag is handled separately.
-      [ "$tag_latest" == true ] && push_image ${target_repo}/${fwdpgrt_image_name}:${latest_tag}
+      [[ "$tag_latest" == true ]] && push_image ${target_repo}/${fwdpgrt_image_name}:${latest_tag}
    fi
 fi
 

=== added file 'tools/scripts/fwd_jdk_query.sh'
--- old/tools/scripts/fwd_jdk_query.sh	1970-01-01 00:00:00 +0000
+++ new/tools/scripts/fwd_jdk_query.sh	2026-03-23 14:26:02 +0000
@@ -0,0 +1,105 @@
+#!/bin/bash
+set -euo pipefail
+
+#set -x
+
+function usage()
+{
+   cat <<EOF
+Usage: $0 [-p <p2j.jar>]
+
+Display JDK used to build FWD
+
+Where:
+       -p <p2j.jar> = The p2j.jar to check, (eg. -p ${p2j_jar})
+       -z <zipfile> = The zipfile containing the p2j.jar to check
+       --decorate = decorate output with \"jdk=<number>\" for parsing
+
+ -? or -h or --help = show usage.
+EOF
+}
+
+function get_jdk()
+{
+   local p=$1
+   echo $(javap -verbose -classpath $p com.goldencode.p2j.Version | awk '/major version/ { print $3 }')
+}
+
+# defaults
+p2j_passed=false
+zipfile_passed=false
+p2j_jar="./build/lib/p2j.jar"
+decorate=false
+
+while getopts ":h?p:z:-:" opt; do
+   case "$opt" in
+      p  ) p2j_jar=$OPTARG
+           p2j_passed=true ;;
+      z  ) zipfile=$OPTARG
+           zipfile_passed=true ;;
+      -  ) case ${OPTARG} in
+             "help" ) usage
+                      exit 1 ;;
+             "decorate" ) decorate=true ;;
+              * ) echo "Unknown option: --${OPTARG}"
+                  usage
+                  exit 1 ;;
+           esac
+           ;;
+      h | \? ) if [[ "$OPTARG" == "?" || "$opt" == "h" ]]; then
+                  usage
+                  exit 1
+               else
+                  echo "Unknown option: -${OPTARG}"
+                  usage
+                  exit 1
+               fi
+               ;;
+   esac
+done
+shift $(($OPTIND - 1))
+
+if ! command -v javap >/dev/null 2>&1; then
+   #echo "ERROR: 'javap' tool is missing. Is JDK installed?" >&2
+   major=0
+else
+   # Extract major version number
+   if [ "$zipfile_passed" == false ]; then
+      major=$(get_jdk "$p2j_jar")
+   elif [ "$p2j_passed" == false ] && [ "$zipfile_passed" == true ]; then
+      tmpjardir=$(mktemp -d p2jjarXXX)
+      cleanup() { rm -fr "$tmpjardir"; }
+      trap cleanup EXIT
+      tmpjar=$(unzip -l "$zipfile" | grep p2j.jar | awk '{ print $4 }')
+      unzip -j "$zipfile" "$tmpjar" -d "$tmpjardir" >/dev/null 2>&1
+      p2j_jar=${tmpjardir}/"p2j.jar"
+      major=$(get_jdk "$p2j_jar")
+   elif [ "$p2j_passed" == true ] && [ "$zipfile_passed" == true ]; then
+      #echo "ERROR: Only pass a zipfile or the P2J parameter." >&2
+      major=0
+   fi
+fi
+
+# Lookup table: class file major version -> JDK label
+declare -A JDK_MAP=(
+   [0]="unknown"
+   [52]="8"
+   [53]="9"
+   [54]="10"
+   [55]="11"
+   [56]="12"
+   [57]="13"
+   [58]="14"
+   [59]="15"
+   [60]="16"
+   [61]="17"
+   [62]="18"
+   [63]="19"
+   [64]="20"
+   [65]="21"
+)
+
+jdk_label="${JDK_MAP[$major]:-UNKNOWN}"
+
+[ "$decorate" == true ] && decor="jdk=" || decor=""
+echo ${decor}${jdk_label}

