Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# Optional: public base URL used when Photopea must load temporary design assets
# Example for a tunnel or VPS reverse proxy:
# OPENMOCKUP_PUBLIC_BASE_URL=https://openmockup.example.com
# PSD / Photopea mode
# `npm run dev:public` creates a separate localhost-only asset server and tunnels
# only that asset server. The OpenMockup Studio Vite UI remains on 127.0.0.1.
# These internal values are normally generated by scripts/dev-public.mjs and
# should not be set manually for the standard local workflow.
# OPENMOCKUP_ASSET_PORT=5174
# OPENMOCKUP_ASSET_SERVER_URL=http://127.0.0.1:5174
# OPENMOCKUP_ASSET_TOKEN=<generated-random-token>
# OPENMOCKUP_PUBLIC_BASE_URL=https://example.trycloudflare.com

# Optional limits for the temporary Vite design endpoint used by PSD/Photopea mode
# Limits for temporary design assets exposed to Photopea.
OPENMOCKUP_MAX_DESIGN_MB=50
OPENMOCKUP_DESIGN_TTL_MS=1800000
# Keep disabled for local/tunnel use. Enable only behind your own access controls.

# Legacy/self-hosted Vite design endpoint only. Keep disabled unless the entire
# deployment is protected by authentication, a private network, or equivalent controls.
OPENMOCKUP_ALLOW_PUBLIC_UPLOADS=0
61 changes: 58 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
- "release/**"
- "hardening/**"
pull_request:

permissions:
Expand All @@ -16,16 +17,30 @@ concurrency:

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
name: Check (${{ matrix.label }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
node: 20.19.0
label: ubuntu-node20
- os: ubuntu-latest
node: 22.19.0
label: ubuntu-node22
- os: windows-latest
node: 22.19.0
label: windows-node22
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22
node-version: ${{ matrix.node }}
cache: npm

- name: Install dependencies
Expand All @@ -36,3 +51,43 @@ jobs:

- name: Build static demo
run: npm run build:demo

browser-smoke:
name: Browser export smoke
needs: check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22.19.0
cache: npm

- name: Install dependencies
run: npm ci

- name: Build demo at root path
run: npm run build:demo
env:
OPENMOCKUP_BASE_PATH: /

- name: Install pinned browser test runtime
shell: bash
run: |
PLAYWRIGHT_ROOT="$RUNNER_TEMP/openmockup-playwright"
mkdir -p "$PLAYWRIGHT_ROOT"
npm install --prefix "$PLAYWRIGHT_ROOT" --no-save --package-lock=false --ignore-scripts playwright@1.55.0
echo "OPENMOCKUP_PLAYWRIGHT_ROOT=$PLAYWRIGHT_ROOT" >> "$GITHUB_ENV"
echo "OPENMOCKUP_PLAYWRIGHT_MODULE=$PLAYWRIGHT_ROOT/node_modules/playwright/index.mjs" >> "$GITHUB_ENV"
echo "PLAYWRIGHT_BROWSERS_PATH=$PLAYWRIGHT_ROOT/browsers" >> "$GITHUB_ENV"

- name: Install Chromium
shell: bash
run: "$OPENMOCKUP_PLAYWRIGHT_ROOT/node_modules/.bin/playwright install --with-deps chromium"

- name: Run browser export smoke
run: node scripts/e2e-demo.mjs
34 changes: 28 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ jobs:
windows-portable:
runs-on: windows-latest
timeout-minutes: 25
env:
CLOUDFLARED_VERSION: "2026.8.3"
CLOUDFLARED_WINDOWS_AMD64_SHA256: "83e726ed18ea78c5ad5213c4c3a3a27051393950d2bc8ed4de69bec12d14eaae"
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 1

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 22
node-version: 22.19.0
cache: npm

- name: Install dependencies
Expand All @@ -28,7 +33,7 @@ jobs:
- name: Verify project
run: npm run check

- name: Resolve and verify release version
- name: Resolve and verify release source
shell: pwsh
run: |
$package = Get-Content package.json -Raw | ConvertFrom-Json
Expand All @@ -37,7 +42,17 @@ jobs:
if ($env:GITHUB_REF_NAME -ne $expectedBranch) {
throw "Release branch $env:GITHUB_REF_NAME does not match package.json version $expectedBranch"
}

git fetch origin main --depth=1
if ($LASTEXITCODE -ne 0) { throw "Could not fetch main for release source verification." }
$mainSha = (git rev-parse FETCH_HEAD).Trim()
$headSha = (git rev-parse HEAD).Trim()
if ($headSha -ne $mainSha) {
throw "Release branch must point at the current main commit. release=$headSha main=$mainSha"
}

"RELEASE_TAG=$releaseTag" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"RELEASE_SHA=$headSha" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Assemble portable Windows package
shell: pwsh
Expand Down Expand Up @@ -76,9 +91,16 @@ jobs:
Copy-Item "packaging\windows\start-openmockup-psd.bat" (Join-Path $stageRoot "start-openmockup-psd.bat")
Copy-Item "packaging\windows\README.txt" (Join-Path $stageRoot "README-PORTABLE.txt")

Invoke-WebRequest `
-Uri "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe" `
-OutFile (Join-Path $stageRoot "tools\cloudflared.exe")
$cloudflaredPath = Join-Path $stageRoot "tools\cloudflared.exe"
$cloudflaredUrl = "https://github.com/cloudflare/cloudflared/releases/download/$env:CLOUDFLARED_VERSION/cloudflared-windows-amd64.exe"
Invoke-WebRequest -Uri $cloudflaredUrl -OutFile $cloudflaredPath

$cloudflaredHash = (Get-FileHash $cloudflaredPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($cloudflaredHash -ne $env:CLOUDFLARED_WINDOWS_AMD64_SHA256) {
throw "cloudflared checksum mismatch. expected=$env:CLOUDFLARED_WINDOWS_AMD64_SHA256 actual=$cloudflaredHash"
}
& $cloudflaredPath --version
if ($LASTEXITCODE -ne 0) { throw "Pinned cloudflared binary did not execute successfully." }

$zipPath = Join-Path $env:GITHUB_WORKSPACE "$packageName.zip"
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
Expand All @@ -104,6 +126,6 @@ jobs:
"OpenMockup-Studio-Windows-x64.zip" `
"OpenMockup-Studio-Windows-x64.zip.sha256" `
--repo $env:GITHUB_REPOSITORY `
--target main `
--target $env:RELEASE_SHA `
--title "OpenMockup Studio $env:RELEASE_TAG" `
--generate-notes
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ The project follows a lightweight changelog format.

## Unreleased

### Security and privacy

- Isolated PSD/Photopea design assets from the local Vite application server. `npm run dev:public` now tunnels only a dedicated asset server on a separate localhost port; the OpenMockup UI itself is never exposed through the temporary Cloudflare Tunnel.
- Added a per-run cryptographically random token between the localhost Vite proxy and the isolated asset server. Public access is limited to unpredictable `GET /design/<id>` asset URLs.
- Restricted the isolated asset server to image uploads, bounded upload size, short-lived in-memory assets, and a minimal route surface.

### Reliability and release hardening

- Expanded CI to Node.js 20.19, Node.js 22.19, and Windows Node.js 22.19.
- Added a Chromium browser smoke test covering real file selection, preview rendering, preset download, and batch ZIP export.
- Release branches must now point at the exact current `main` commit before packaging; release tags target the verified commit SHA instead of a mutable branch name.
- Pinned the bundled Windows `cloudflared` binary to an explicit version and SHA-256 checksum instead of downloading `latest` without verification.

### Performance

- Bounded retained batch-preview memory after ZIP creation. Only the first 12 rendered previews keep their Blob/Object URL; additional completed results are released after the archive is generated while export/history counts remain intact.
- Enabled JSZip streaming mode during ZIP generation.

### Maintenance

- Updated `@types/node` to 26.4.0 and `@vitejs/plugin-react` to 6.1.1.

## 0.19.0 - 2026-08-27

### Editing and demo
Expand Down Expand Up @@ -38,7 +60,7 @@ The project follows a lightweight changelog format.
### Repository and contributor experience

- Refreshed the README around the batch-mockup use case and faster onboarding.
- Added a single npm run check quality gate used locally and in CI.
- Added a single `npm run check` quality gate used locally and in CI.
- Added contribution, security, architecture, roadmap, issue, and pull-request guidance.
- Grouped GitHub Actions dependency updates to reduce maintenance noise.
- Improved page title and social/SEO metadata for hosted deployments.
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

### [▶ Try OpenMockup Studio in your browser](https://slp-dev1.github.io/OpenMockup-Studio/)

The public demo needs no installation and supports PNG, JPG, and WebP mockups. Click **Try sample project** to see it working immediately. Files stay in the browser. PSD Smart Objects require the local version because that workflow uses Photopea and a temporary asset endpoint.
The public demo needs no installation and supports PNG, JPG, and WebP mockups. Click **Try sample project** to see it working immediately. Files stay in the browser. PSD Smart Objects require the local version because that workflow uses Photopea and a temporary public asset URL.

![OpenMockup Studio interface](docs/screenshot.png)

Expand All @@ -29,6 +29,7 @@ OpenMockup Studio is built for Etsy, WooCommerce, marketplace sellers, artists,
- **Visual placement editor:** move, scale, rotate, anchor, fit, adjust opacity, or drag perspective corners.
- **Marketplace-ready exports:** crop, resize, watermark, rename, convert, and ZIP results.
- **Collision-safe batches:** duplicate output names are preserved automatically instead of being overwritten.
- **Bounded batch gallery:** large exports do not keep every rendered image alive after ZIP creation.
- **Local-first workflow:** flat-image rendering stays in your browser.
- **Self-hostable:** no account or hosted service is required.

Expand All @@ -50,10 +51,10 @@ The demo intentionally disables PSD uploads. Use the Windows package or develope
| Launcher | Use it for |
| --- | --- |
| `start-openmockup.bat` | PNG, JPG, and WebP mockups; local image-mode server |
| `start-openmockup-psd.bat` | PSD mode with Photopea and the bundled temporary asset tunnel helper |
| `start-openmockup-psd.bat` | PSD mode with Photopea and an isolated temporary design-asset tunnel |
| `stop.bat` | Stop a remaining local server/tunnel process |

The portable package includes its own runtime, dependencies, and PSD-mode tunnel helper. **You do not need to install Node.js, npm, or cloudflared.** The app opens at `http://127.0.0.1:5173`; keep the launcher window open while you work.
The portable package includes its own runtime, dependencies, and pinned PSD-mode tunnel helper. **You do not need to install Node.js, npm, or cloudflared.** The app opens at `http://127.0.0.1:5173`; keep the launcher window open while you work.

### Developers / source install

Expand All @@ -70,6 +71,8 @@ Source development requires Node.js 20.19+ (Node 22 recommended). For PSD suppor
npm run dev:public
```

`dev:public` keeps Vite on localhost and starts a second, minimal localhost asset server. Cloudflare tunnels only that second port. The browser posts designs to the local Vite proxy; Vite forwards them to the isolated server with a per-run random token. Photopea receives only the resulting temporary public design URL.

## Typical workflow

1. Add one or more PSD, PNG, JPG, or WebP mockups.
Expand All @@ -87,7 +90,8 @@ npm run dev:public
| PSD Smart Objects | — | Yes |
| 4-corner perspective | Yes | — |
| Rendering | Browser Canvas | Photopea iframe |
| Public asset URL needed | No | Yes |
| Public asset URL needed | No | Yes, temporary design asset only |
| App UI publicly tunneled | No | No |
| Batch ZIP export | Yes | Yes |
| Account required | No | No |
| Public demo | Yes | No |
Expand All @@ -96,17 +100,19 @@ npm run dev:public

Flat image rendering stays in the browser. That includes the public GitHub Pages demo: there is no upload backend in the static demo build.

PSD mode is different: Photopea must be able to download the selected design from a public HTTPS address. `start-openmockup-psd.bat`, `start.bat`, or `npm run dev:public` creates a temporary Cloudflare Tunnel for that asset flow. The OpenMockup UI itself remains on localhost.
PSD mode is different: Photopea must be able to download the selected transformed design from a public HTTPS address. `start-openmockup-psd.bat` or `npm run dev:public` creates a temporary Cloudflare Tunnel **only to an isolated design-asset server**. The OpenMockup Studio UI and Vite server remain bound to localhost and are not routed through that tunnel.

The isolated server stores temporary images in memory, limits their size and lifetime, and exposes only unpredictable read URLs. The browser-to-asset upload path stays local and is authenticated with a random per-run token between local processes.

Do not use PSD mode for confidential assets unless you understand and accept this data flow. OpenMockup Studio is not affiliated with Photopea.
Do not use PSD mode for confidential assets unless you understand and accept that the transformed design must be retrievable by Photopea over the temporary public URL. OpenMockup Studio is not affiliated with Photopea.

## Commands

| Command | Purpose |
| --- | --- |
| `npm run dev` | Start local development mode |
| `npm start` | Start on `127.0.0.1:5173` |
| `npm run dev:public` | Start with a temporary Cloudflare Tunnel |
| `npm run dev:public` | Start PSD mode with an isolated temporary design-asset tunnel |
| `npm run typecheck` | Run TypeScript checks |
| `npm run typecheck:strict` | Include unused-code checks |
| `npm test` | Run the automated test suite once |
Expand All @@ -120,34 +126,28 @@ Do not use PSD mode for confidential assets unless you understand and accept thi

Flat image mode can be hosted as a static Vite build. The repository includes a GitHub Pages workflow that builds in `demo` mode, uses the repository subpath as Vite's base path, omits the temporary PSD design-server plugin, and deploys `dist/` as a Pages artifact.

PSD mode additionally needs the temporary-design endpoint used by the Photopea bridge.

For a server deployment:

```bash
npm ci
npm run build
npm run preview
```
For ordinary local PSD use, prefer `npm run dev:public` rather than exposing the Vite server itself. A production/server deployment that offers PSD assets to remote users needs a deliberately authenticated asset service with rate limits instead of simply setting `OPENMOCKUP_ALLOW_PUBLIC_UPLOADS=1` on an Internet-facing Vite instance.

Example environment variables are documented in [`.env.example`](.env.example). Public uploads are disabled by default; only enable them behind authentication, a private network, or equivalent access controls.
Example environment variables are documented in [`.env.example`](.env.example).

## Troubleshooting PSD mode

**Photopea cannot fetch the design:** use `start-openmockup-psd.bat`, `start.bat`, or `npm run dev:public`; localhost-only asset URLs are not reachable by Photopea.
**Photopea cannot fetch the design:** use `start-openmockup-psd.bat` or `npm run dev:public`. Do not create a tunnel directly to the Vite UI port.

**A `trycloudflare.com` address expired:** open the app at `http://127.0.0.1:5173`, stop the previous process, then start PSD mode again to create a new temporary tunnel.
**A `trycloudflare.com` address expired:** stop the previous PSD-mode process and start it again. Continue using the OpenMockup UI at `http://127.0.0.1:5173`; the temporary public URL is for design assets, not for browsing the app.

**A Smart Object is not detected:** verify that the PSD/PSB really contains a Smart Object, unlock unusual nested layers where possible, and simplify highly unusual document structures before retrying.

PSD batches run serially to keep the Photopea session stable.

## Project health

The repository uses strict TypeScript checks, Vitest, production and demo builds in GitHub Actions, Dependabot, and versioned Windows release builds with SHA-256 checksums. Core placement, perspective, naming, mockup, persistence, cache, PSB parsing, and export behavior has automated coverage.
The repository uses strict TypeScript checks, Vitest, production and demo builds, Node 20/22 Linux CI, Windows CI, a Chromium browser export smoke, Dependabot, and versioned Windows release builds with SHA-256 checksums. The bundled `cloudflared` release helper is pinned and checksum-verified. Core placement, perspective, naming, mockup, persistence, cache, PSB parsing, asset isolation, and export behavior has automated coverage.

- [Roadmap](docs/ROADMAP.md)
- [Architecture](docs/ARCHITECTURE.md)
- [Releasing](docs/RELEASING.md)
- [Recommended repository rules](docs/REPOSITORY_SETTINGS.md)
- [Contributing](CONTRIBUTING.md)
- [Security policy](SECURITY.md)
- [Changelog](CHANGELOG.md)
Expand Down
Loading