Files
Password-Manager/delphi-backend/assets/BuildAssets.ps1
T
r-zakarya 5fc07aed7a refactor(js): extract TOTP module + add RFC 6238 tests (§3.1)
Fourth slice of the app.js split. Moves TOTP (base32Decode, generateTOTP,
parseOtpAuthUri) plus the TOTP-secret and custom-field AES-GCM wrappers to
js/app.totp.js. Loads before app.js (pure declarations), after app.crypto.js
(uses encryptPwd/decryptPwd). Also called by app.import.js and app.sync.js
via shared global scope.

- Byte-for-byte identical extraction; no duplicate const; syntax OK on all
  five app parts.
- NEW: js/tests/totp.test.js — 13 tests including the 5 RFC 6238 Appendix B
  reference vectors (generateTOTP reads Date.now(), so each case stubs the
  sandbox clock to the vector's fixed time), base32 decode edge cases, and
  parseOtpAuthUri. Extraction AND new coverage in one slice.
- Suite: 42 → 55 tests, all green.
- Assets regenerated (manifest now embeds all 6 ordered JS files:
  argon2 → crypto → totp → import → app → sync); also fixes the previous
  import commit's not-yet-rebuilt manifest.
- Delphi build artifacts (*.vrc, *.$manifest) gitignored.

app.js: 11936 → 10138 lines (4 modules extracted, ~1800 lines).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:35:29 +01:00

197 lines
7.7 KiB
PowerShell

# 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\argon2.js',
'js\app.crypto.js',
'js\app.totp.js',
'js\app.import.js',
'js\app.js',
'js\app.sync.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) }
# --- JS syntax gate -----------------------------------------------------------
# A syntax error in app.js parses fine here but kills the whole frontend at
# runtime (no event handlers → dead UI), and it's only caught after a full
# Delphi rebuild. Run `node --check` on every embedded .js so a broken bundle
# never makes it into assets.res. Node is optional: if it isn't installed we
# warn and continue rather than blocking the build on a machine without it.
$node = Get-Command node.exe -ErrorAction SilentlyContinue
if (-not $node) { $node = Get-Command node -ErrorAction SilentlyContinue }
$jsFiles = $files | Where-Object { $_.Relative -match '\.js$' }
if ($jsFiles) {
if ($node) {
foreach ($jf in $jsFiles) {
Log "Syntax check: $($jf.Relative)"
# --check prints errors to stderr and returns non-zero on failure.
# Pipe $null into node so it can NEVER block waiting on stdin
# (some Windows node shims read stdin when launched from a
# non-interactive pre-build event, which would hang the build).
$out = $null | & $node.Source --check $jf.FullPath 2>&1
if ($LASTEXITCODE -ne 0) {
Log "JS SYNTAX ERROR in $($jf.Relative):"
Log ($out | Out-String)
throw "JS syntax check failed for $($jf.Relative) - aborting asset build."
}
}
Log "JS syntax OK."
# --- Unit test gate ---------------------------------------------------
# Run the frontend regression suite (crypto round-trip, CSV import,
# sync-merge arbitration) before embedding. A broken crypto/merge
# invariant now blocks the build the same way a syntax error does,
# instead of surfacing only after a full Delphi rebuild + manual test.
# ~1.7 s, zero deps (node:test). Skipped automatically if the suite
# isn't present. Set PM_SKIP_TESTS=1 to bypass during rapid iteration.
if ($env:PM_SKIP_TESTS -eq '1') {
Log "PM_SKIP_TESTS=1 - skipping unit tests."
} elseif (Test-Path (Join-Path $WebRoot 'js\tests')) {
Log "Running frontend unit tests..."
Push-Location $WebRoot
try {
$testOut = $null | & $node.Source --test 'js/tests/**/*.test.js' 2>&1
$testExit = $LASTEXITCODE
} finally {
Pop-Location
}
if ($testExit -ne 0) {
Log "UNIT TESTS FAILED:"
Log ($testOut | Out-String)
throw "Frontend unit tests failed - aborting asset build. (Set PM_SKIP_TESTS=1 to bypass.)"
}
Log "Unit tests OK."
}
} else {
Log "WARNING: node not found - skipping JS syntax check + unit tests. Install Node to enable them."
}
}
# --- 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
}