feat: Delphi backend + JS↔Delphi bridge (clipboard, tray, auto-lock)
Introduces the Delphi 12 FMX backend (PMServer) that hosts the embedded
WebView2 vault on 127.0.0.1, and a native bridge between JS and Delphi
that wires three privacy-focused features:
1. Secure clipboard
Copying a password registers the Win32 "ExcludeClipboardContentFromMonitorProcessing"
format alongside CF_UNICODETEXT, so Win+V clipboard history never sees
the value. Auto-clears after 30s via TTimer. Bridge.copySecure() in
app.js routes all password/username/secret copy paths through the
native layer when running inside the Delphi WebView2 (falls back to
navigator.clipboard for the PHP standalone).
2. Tray icon (X-to-tray when server running)
Closing the dev panel hides both the form HWND and the TFMAppClass
per-process proxy window that owns the FMX taskbar entry — the form's
HWND alone is not the taskbar-visible one in FMX (took some iteration
to discover). Tray menu: Open, Lock vault, Quit. Clipboard is force-
cleared on minimize as extra safety. First-time minimize fires a
balloon notification so the user knows the app is still running.
3. Auto-lock on Windows session lock (Win+L)
wtsapi32.dll!WTSRegisterSessionNotification on a dedicated message-only
window. On WM_WTSSESSION_CHANGE / WTS_SESSION_LOCK, the bridge calls
ExecuteJavaScript('lockVault()'). Same path used by the tray "Lock vault"
menu item.
Bridge architecture:
- JS → Delphi via cmd:// URLs intercepted in OnBeforeNavigate
(pattern lifted from DeskInsight Monaco). Currently exposes
cmd://clipboard/copy?text=...&clear=... and cmd://clipboard/clear.
- Delphi → JS via TTMSFNCWebBrowser.ExecuteJavaScript with guarded
calls (typeof check) so the bridge degrades cleanly if app.js isn't
loaded yet.
Files:
- Source/PM.Bridge.pas (new) — TSecureClipboard + TPMBridge
- UMainForm.pas/.fmx — bridge wiring, FormCloseQuery intercept, tray
callbacks (BridgeTrayRestore / BridgeLockRequest / BridgeQuit)
- js/app.js — Bridge object, 5 navigator.clipboard sites migrated to
Bridge.copySecure with PHP-compatible fallback, Bridge.onTrayRestore
handler that resets the auto-lock timer
.gitignore extended with Delphi build artifacts (*.dcu, Win32/, Win64/,
__history/, __recovery/, *.identcache, *.dsk, *.local, etc.) so source
checkouts stay clean.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
REM Wrapper around BuildAssets.ps1 — captures stdout+stderr to build.log
|
||||
REM so Delphi's pre-build event can call this without worrying about
|
||||
REM cmd.exe '&' escape rules.
|
||||
REM
|
||||
REM Usage in Delphi pre-build event:
|
||||
REM "Z:\password-manager\delphi-backend\assets\BuildAssets.cmd"
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "LOG=%SCRIPT_DIR%build.log"
|
||||
|
||||
echo === BuildAssets.cmd at %DATE% %TIME% === > "%LOG%"
|
||||
echo Calling PowerShell... >> "%LOG%"
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%BuildAssets.ps1" >> "%LOG%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
echo. >> "%LOG%"
|
||||
echo Exit code: %RC% >> "%LOG%"
|
||||
|
||||
exit /b %RC%
|
||||
@@ -0,0 +1,135 @@
|
||||
# BuildAssets.ps1
|
||||
# Generates assets.rc + assets.res containing the static web assets
|
||||
# (index.html, js/*, css/*) so they can be linked into PMServer.exe as
|
||||
# Win32 RCDATA resources.
|
||||
#
|
||||
# Workflow:
|
||||
# 1. Edit Z:\password-manager\index.html / js\app.js / css\style.css
|
||||
# 2. Run this script (or set it as pre-build event in Delphi)
|
||||
# 3. Build PMServer.dpr in Delphi
|
||||
# 4. Run - exe is autonomous, no external files needed at runtime
|
||||
#
|
||||
# Resource naming: URL path '/js/app.js' -> resource 'JS_APP_JS'
|
||||
# - strip leading '/'
|
||||
# - replace '/' '\' '.' '-' with '_'
|
||||
# - uppercase
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $WebRoot = '',
|
||||
[string] $OutRC = '',
|
||||
[string] $OutRES = '',
|
||||
[string] $OutInc = ''
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Resolve $PSScriptRoot — may be empty depending on invocation context.
|
||||
# Fallback to the script's own file path.
|
||||
$ScriptDir = $PSScriptRoot
|
||||
if (-not $ScriptDir) { $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition }
|
||||
if (-not $ScriptDir) { $ScriptDir = (Get-Location).Path }
|
||||
|
||||
# Apply param defaults now that we have a valid script dir
|
||||
if (-not $WebRoot) { $WebRoot = Join-Path $ScriptDir '..\..' }
|
||||
if (-not $OutRC) { $OutRC = Join-Path $ScriptDir 'assets.rc' }
|
||||
if (-not $OutRES) { $OutRES = Join-Path $ScriptDir 'assets.res' }
|
||||
if (-not $OutInc) { $OutInc = Join-Path $ScriptDir 'assets.inc' }
|
||||
|
||||
# All output goes to stdout/stderr. When invoked via BuildAssets.cmd the
|
||||
# wrapper captures both streams into build.log; when run interactively
|
||||
# everything shows in the terminal.
|
||||
|
||||
function Log { param([string] $msg) Write-Host $msg }
|
||||
|
||||
try {
|
||||
|
||||
$WebRoot = (Resolve-Path $WebRoot).Path
|
||||
Log "Web root: $WebRoot"
|
||||
|
||||
# Whitelist patterns (relative to WebRoot). Add more here when needed.
|
||||
# Use exact paths (not wildcards) to avoid embedding *-legacy.* backups.
|
||||
$patterns = @(
|
||||
'index.html',
|
||||
'js\app.js',
|
||||
'css\style.css'
|
||||
)
|
||||
|
||||
# Discover files
|
||||
$files = @()
|
||||
foreach ($p in $patterns) {
|
||||
$found = Get-ChildItem -Path (Join-Path $WebRoot $p) -File -ErrorAction SilentlyContinue
|
||||
foreach ($f in $found) {
|
||||
$rel = $f.FullName.Substring($WebRoot.Length).TrimStart('\','/')
|
||||
$files += [pscustomobject]@{
|
||||
FullPath = $f.FullName
|
||||
Relative = $rel
|
||||
UrlPath = '/' + ($rel -replace '\\','/')
|
||||
ResName = ($rel -replace '[\\/\.\-]','_').ToUpperInvariant()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($files.Count -eq 0) {
|
||||
throw "No assets found under $WebRoot. Check the patterns."
|
||||
}
|
||||
|
||||
Log "Embedding $($files.Count) file(s):"
|
||||
$files | ForEach-Object { Log (" " + $_.UrlPath + " -> " + $_.ResName) }
|
||||
|
||||
# --- Generate assets.rc -------------------------------------------------------
|
||||
$rc = New-Object System.Text.StringBuilder
|
||||
[void]$rc.AppendLine('// Auto-generated by BuildAssets.ps1 - do not edit by hand.')
|
||||
[void]$rc.AppendLine('#pragma code_page(65001)')
|
||||
[void]$rc.AppendLine('')
|
||||
foreach ($f in $files) {
|
||||
# brcc32 accepts forward slashes; backslashes need to be doubled in C strings
|
||||
$rcPath = $f.FullPath -replace '\\','\\'
|
||||
[void]$rc.AppendLine("$($f.ResName) RCDATA `"$rcPath`"")
|
||||
}
|
||||
[System.IO.File]::WriteAllText($OutRC, $rc.ToString(), [System.Text.Encoding]::ASCII)
|
||||
Log "Wrote $OutRC"
|
||||
|
||||
# --- Generate assets.inc (Pascal include with manifest) -----------------------
|
||||
# Compile-time mapping URL -> resource name, consumed by PM.EmbeddedAssets.pas.
|
||||
$nl = [Environment]::NewLine
|
||||
$inc = New-Object System.Text.StringBuilder
|
||||
[void]$inc.Append('// Auto-generated by BuildAssets.ps1 - do not edit by hand.' + $nl)
|
||||
[void]$inc.Append('const' + $nl)
|
||||
[void]$inc.Append(' EMBEDDED_ASSET_COUNT = ' + $files.Count + ';' + $nl)
|
||||
[void]$inc.Append(' EMBEDDED_ASSETS: array[0..EMBEDDED_ASSET_COUNT-1] of TEmbeddedAsset = (' + $nl)
|
||||
for ($i = 0; $i -lt $files.Count; $i++) {
|
||||
$f = $files[$i]
|
||||
if ($i -eq $files.Count - 1) { $sep = '' } else { $sep = ',' }
|
||||
$line = " (UrlPath: '" + $f.UrlPath + "'; ResName: '" + $f.ResName + "')" + $sep + $nl
|
||||
[void]$inc.Append($line)
|
||||
}
|
||||
[void]$inc.Append(' );' + $nl)
|
||||
[System.IO.File]::WriteAllText($OutInc, $inc.ToString(), [System.Text.Encoding]::UTF8)
|
||||
Log "Wrote $OutInc"
|
||||
|
||||
# --- Compile to .res via brcc32 ----------------------------------------------
|
||||
$brcc = $null
|
||||
if ($env:BDS) {
|
||||
$candidate = Join-Path $env:BDS 'bin\brcc32.exe'
|
||||
if (Test-Path $candidate) { $brcc = $candidate }
|
||||
}
|
||||
if (-not $brcc) {
|
||||
$cmd = Get-Command brcc32.exe -ErrorAction SilentlyContinue
|
||||
if ($cmd) { $brcc = $cmd.Source }
|
||||
}
|
||||
if (-not $brcc) {
|
||||
throw "brcc32.exe not found. Set the BDS environment variable to your Delphi install root, or add brcc32.exe to PATH."
|
||||
}
|
||||
Log "Using $brcc"
|
||||
|
||||
& $brcc -32 -fo "$OutRES" "$OutRC"
|
||||
if ($LASTEXITCODE -ne 0) { throw "brcc32 failed with exit code $LASTEXITCODE" }
|
||||
Log "Wrote $OutRES"
|
||||
Log "OK"
|
||||
|
||||
} catch {
|
||||
Log "FATAL: $($_.Exception.Message)"
|
||||
Log "Stack: $($_.ScriptStackTrace)"
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Auto-generated by BuildAssets.ps1 - do not edit by hand.
|
||||
const
|
||||
EMBEDDED_ASSET_COUNT = 3;
|
||||
EMBEDDED_ASSETS: array[0..EMBEDDED_ASSET_COUNT-1] of TEmbeddedAsset = (
|
||||
(UrlPath: '/index.html'; ResName: 'INDEX_HTML'),
|
||||
(UrlPath: '/js/app.js'; ResName: 'JS_APP_JS'),
|
||||
(UrlPath: '/css/style.css'; ResName: 'CSS_STYLE_CSS')
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
// Auto-generated by BuildAssets.ps1 - do not edit by hand.
|
||||
#pragma code_page(65001)
|
||||
|
||||
INDEX_HTML RCDATA "Z:\\password-manager\\index.html"
|
||||
JS_APP_JS RCDATA "Z:\\password-manager\\js\\app.js"
|
||||
CSS_STYLE_CSS RCDATA "Z:\\password-manager\\css\\style.css"
|
||||
Binary file not shown.
Reference in New Issue
Block a user