Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5e5e890
fix(ai): Resolve issue #1958 - Extract the UI-agnostic local setup en…
Aug 29, 2026
c5b383c
fix(ai): Resolve issue #1959 - Add the desktop-shaped UI mode and ins…
Aug 29, 2026
7ba9aa5
fix(ai): Resolve issue #1954 - Create a shared ProPR API client and i…
Aug 29, 2026
356bfce
fix(ai): Resolve issue #1955 - Add secure desktop pairing tokens and …
Aug 29, 2026
5fc195c
fix(ai): Resolve issue #1956 - Scaffold the secure Electron desktop r…
Aug 29, 2026
b08e5f1
feat(ai): Implemented the follow-up fix without committing.
Aug 29, 2026
56c97eb
feat(ai): Fixed the full-suite failure in [taskInspectCommands.test.t…
Aug 29, 2026
13137ff
Merge pull request #1964 from integry/1959/gpt-5.6-sol-add-the-deskto…
integry Aug 29, 2026
2ee3db9
Merge pull request #1965 from integry/1954/gpt-5.6-sol-create-a-share…
integry Aug 29, 2026
21d0ff3
feat(ai): Implemented the packaging supply-chain follow-up without ch…
Aug 29, 2026
788c4b1
Merge pull request #1966 from integry/1955/gpt-5.6-sol-add-secure-des…
integry Aug 29, 2026
3275d74
feat(ai): Implemented the release-blocking packaging fix.
Aug 29, 2026
f01d661
Merge pull request #1969 from integry/1953-epic-desktop-transport-api
integry Aug 29, 2026
10c0cd7
feat(ai): Implemented the self-contained desktop preparation:
Aug 29, 2026
b20ae26
feat(ai): Implemented the follow-up in [cli-node-compatibility.yml](/…
Aug 29, 2026
6882e61
Merge pull request #1963 from integry/1958/gpt-5.6-sol-extract-the-ui…
integry Aug 29, 2026
c95ad7a
feat(ai): Implemented the Electron entry-format repair and strengthen…
Aug 29, 2026
4606f27
merge: resolve conflicts from 1950-epic-cross-platform-dsk into 1951-…
Aug 29, 2026
79e5111
feat(ai): Fixed the PR build failure in [App.tsx](/tmp/git-processor/…
Aug 29, 2026
c706970
feat(ai): Implemented the Linux space-free package path while preserv…
Aug 29, 2026
d1d4e32
feat(ai): Fixed the full-suite failure in [test/orchestratorConfig.te…
Aug 29, 2026
5b42070
feat(ai): Implemented only F1 and F2.
Aug 29, 2026
9e9cd23
feat(ai): Implemented F1–F3 only.
Aug 29, 2026
c505632
feat(ai): Implemented F3, F4, and F5 only.
Aug 29, 2026
7c39c48
feat(ai): Implemented only F4, F5, and F6.
Aug 29, 2026
944d55e
feat(ai): Implemented F6 only.
Aug 29, 2026
6ed84df
feat(ai): Implemented F7 only.
Aug 29, 2026
7a26a6b
feat(ai): Implemented only F7, F8, and F9.
Aug 29, 2026
1da60f5
feat(ai): Fixed the intermittent notification regression failure in […
Aug 29, 2026
5b73b8f
feat(ai): Fixed the flaky full-suite failure in [webPushDispatcher.te…
Aug 29, 2026
0728353
feat(ai): Implemented the focused PR #1968 follow-up without committing.
Aug 29, 2026
85aee4b
Merge pull request #1967 from integry/1956/gpt-5.6-sol-scaffold-the-s…
integry Aug 29, 2026
a9fde0f
Merge remote-tracking branch 'origin/1950-epic-cross-platform-dsk' in…
Aug 29, 2026
ba2511b
feat(ai): Implemented F9 and S1 follow-up changes.
Aug 29, 2026
4c99bb7
feat(ai): Implemented the requested follow-ups on synced head `a9fde0…
Aug 29, 2026
e319d2e
feat(ai): Fixed the CI failure in [App.tsx](/tmp/git-processor/worktr…
Aug 29, 2026
337f49c
Merge pull request #1968 from integry/1951-epic-desktop-experience-uxs
integry Aug 29, 2026
750e802
feat(ai): Implemented F2 only.
Aug 29, 2026
b3738cf
merge: resolve conflicts from 1950-epic-cross-platform-dsk into 1952-…
Aug 29, 2026
8ef74c1
feat(ai): Fixed the CI blocker in [App.tsx](/tmp/git-processor/worktr…
Aug 29, 2026
7c8010f
Merge pull request #1974 from integry/1952-epic-electron-runtime-app
integry Aug 29, 2026
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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@ DASHBOARD_API_PORT=4000
# security). Defaults to http://localhost:4000 when unset; set it to the
# https://t-<id>.propr.dev host when the hosted UI tunnel is enabled.
# API_PUBLIC_URL=http://localhost:4000
# Optional lifetime for newly paired desktop instance tokens. When unset,
# tokens remain valid until the owner revokes them. Range: 1-3650 days.
# PROPR_DESKTOP_TOKEN_TTL_DAYS=90
# Optional per-IP desktop discovery/pairing quotas. Defaults are documented in
# docs/docs/operations/desktop-pairing.md.
# PROPR_DISCOVERY_RATE_LIMIT_MAX=60
# PROPR_PAIRING_START_RATE_LIMIT_MAX=10
# PROPR_PAIRING_POLL_RATE_LIMIT_MAX=180
# Session cookie domain. Leave UNSET for v1 — including hosted UI tunnel proxy
# sessions, which run on a single t-<id>.propr.dev host (see the tunnel
# section above). Only set it for a custom multi-subdomain deployment.
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/cli-node-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- '.github/workflows/cli-node-compatibility.yml'
- 'package-lock.json'
- 'packages/cli/**'
- 'packages/local-setup/**'
- 'packages/shared/**'

concurrency:
Expand Down Expand Up @@ -37,8 +38,10 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Build shared dependency
run: npm run build -w @propr/shared
- name: Build workspace dependencies
run: |
npm run build -w @propr/shared
npm run build -w @propr/local-setup

- name: Run project option regressions
run: >-
Expand Down
71 changes: 71 additions & 0 deletions .github/workflows/desktop-release-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Desktop Release Guard

on:
pull_request:
paths:
- '.github/workflows/desktop-release-guard.yml'
- 'apps/desktop/**'
- 'package.json'
- 'package-lock.json'
- 'packages/client/**'
- 'packages/shared/**'
- 'propr-ui/**'
push:
tags:
- 'v*'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: desktop-release-guard-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Audit and package desktop app
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version-file: '.nvmrc'
cache: npm
cache-dependency-path: package-lock.json

# Audit the committed resolution before npm lifecycle or packaging code can run.
- name: Audit production runtime dependencies (low threshold)
run: npm run audit:runtime

- name: Audit desktop packaging toolchain (high threshold)
run: npm run desktop:audit:packaging

- name: Install locked dependencies
run: npm ci

- name: Package desktop app from clean checkout
run: |
test ! -e packages/shared/dist
test ! -e packages/client/dist
test ! -e apps/desktop/out
npm run desktop:package

- name: Typecheck desktop and renderer
run: npm run desktop:typecheck

- name: Test desktop runtime
run: npm run desktop:test

- name: Configure Chromium sandbox helper
run: |
sudo chown root:root apps/desktop/out/propr-desktop-linux-x64/chrome-sandbox
sudo chmod 4755 apps/desktop/out/propr-desktop-linux-x64/chrome-sandbox

- name: Launch packaged desktop app with sandboxing
run: xvfb-run --auto-servernum npm run desktop:smoke
29 changes: 24 additions & 5 deletions .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
- name: Build and test the CLI Agent Skill
run: |
npm run build -w @propr/shared
npm run build -w @propr/local-setup
npm run typecheck -w @propr/cli
npx tsx --experimental-test-module-mocks --test \
packages/cli/src/agentSkill.test.ts \
Expand Down Expand Up @@ -82,6 +83,7 @@ jobs:
runuser --user node -- env HOME=/home/node bash -euo pipefail <<'NON_ROOT'
test "$(node -p 'process.geteuid()')" -ne 0
npm run build -w @propr/shared
npm run build -w @propr/local-setup
npx tsx --experimental-test-module-mocks --test \
packages/cli/src/agentSkill.test.ts \
packages/cli/src/agentSkill.forceRace.test.ts \
Expand Down Expand Up @@ -111,6 +113,7 @@ jobs:
test "$(node -p process.platform)" = darwin
test "$(node -p process.arch)" = arm64
npm run build -w @propr/shared
npm run build -w @propr/local-setup
npm run typecheck -w @propr/cli
npx tsx --experimental-test-module-mocks --test \
packages/cli/src/agentSkill.test.ts \
Expand Down Expand Up @@ -281,10 +284,11 @@ jobs:
echo
echo "--- Hosted tunnel regression tests ---"
echo "Running hosted tunnel regression tests..."
# Build @propr/shared first: the tsx and UI tests below import from it,
# so a stale or missing dist in a clean checkout would fail or use old
# output. Build once, up front, before anything that depends on it.
# Build workspace dependencies first: the tsx and UI tests below import
# from them, so a stale or missing dist in a clean checkout would fail
# or use old output. Build once, up front, before their consumers.
npm run build -w @propr/shared
npm run build -w @propr/local-setup
PROPR_DEMO_MODE=true npx tsx --test \
test/orchestratorConfig.test.mjs \
packages/cli/src/commands/setup/engine.test.ts \
Expand Down Expand Up @@ -328,6 +332,7 @@ jobs:
- 'packages/shared/**'
ui:
- 'propr-ui/**'
- 'packages/client/**'
- 'packages/shared/**'
docs:
- 'docs/**'
Expand Down Expand Up @@ -445,6 +450,18 @@ jobs:
EXIT_CODE=1
fi

if [ $UI_FAILED -eq 0 ]; then
CLIENT_OUTPUT=$(npm run typecheck -w @propr/client 2>&1 && npm test -w @propr/client 2>&1 && npm run build -w @propr/client 2>&1) || {
echo "❌ Client Package Validation FAILED (UI transport dependency)" >> build_log.txt
echo "$CLIENT_OUTPUT" >> build_log.txt
UI_FAILED=1
EXIT_CODE=1
}
if [ $UI_FAILED -eq 0 ]; then
echo "✅ Client Package validation passed" >> build_log.txt
fi
fi

if [ $UI_FAILED -eq 0 ]; then
TYPECHECK_OUTPUT=$(npm run typecheck -w propr-ui 2>&1) || {
echo "❌ UI Typecheck FAILED" >> build_log.txt
Expand Down Expand Up @@ -708,8 +725,10 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Build shared package
run: npm run build --workspace=@propr/shared
- name: Build workspace dependencies
run: |
npm run build --workspace=@propr/shared
npm run build --workspace=@propr/local-setup

- name: Parse init JSON output
run: npx tsx --test packages/cli/src/commands/initCommands.test.ts
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,7 @@ apps/release-site-videos/

# Standalone publish staging (scripts/build-publish.mjs)
dist-publish/

# Electron Forge build and package output
apps/desktop/.vite/
apps/desktop/out/
51 changes: 51 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ProPR Desktop

This workspace packages the existing `propr-ui` React source as a sandboxed Electron renderer. The desktop entry is
`propr-ui/src/desktop.tsx`; the normal web entry, service worker, CLI, API, and self-hosted deployment remain unchanged.

## Commands

Run these from the repository root:

```sh
npm run desktop:dev
npm run desktop:typecheck
npm run desktop:test
npm run desktop:package
npm run desktop:smoke # Run under xvfb-run on a headless Linux host.
npm run desktop:make
npm run desktop:audit
# On Linux hosts with the corresponding native packaging tools installed:
npm run make:deb -w @propr/desktop
npm run make:rpm -w @propr/desktop
```

Desktop development, typecheck, package, and make commands build required renderer workspace dependencies through
`desktop:prepare`, in dependency order (`@propr/shared` then `@propr/client`). They do not depend on previously
generated workspace `dist` directories.

Development renderer URLs are accepted only when Electron Forge supplies an HTTP loopback URL. Packaged builds load
the generated renderer from the application ASAR through an app-owned protocol.

The packaged-binary smoke test verifies the hardened fuse states, launches the Linux artifact without a
sandbox-disabling flag, rejects main-process uncaught exceptions, and requires proof that `window.proprDesktop` is
exposed before accepting renderer-ready and a clean exit.

`desktop:audit` deliberately applies separate policies to the two dependency surfaces: low-or-higher advisories fail
the production-runtime audit, while high and critical advisories fail the desktop development/build-tool audit. Release
CI runs both checks directly from the committed lockfile before installing or executing the packaging toolchain.

## Security boundary

The renderer has no Node.js integration and receives only the typed `window.proprDesktop` bridge. It exposes metadata,
validated external-browser opening, profiles, encrypted credentials, lifecycle placeholders, and validated deep-link
events. It never exposes a shell, command runner, arbitrary IPC call, or filesystem path/API.

Profile metadata is stored in an app-owned, permission-restricted JSON file. Credential values are encrypted with
Electron `safeStorage` before they are written separately. If OS encryption is unavailable—or Linux selects the
`basic_text` backend—the app reports that state and refuses to persist or return credentials; there is no plaintext
fallback. Profiles remain usable because they contain only a display label and validated API endpoint.

`propr://connect` and `propr://open` are the only accepted deep-link actions. A single-instance lock routes later
activations to the existing window. Local lifecycle methods intentionally return `not-implemented`; this scaffold does
not download, install, start, or execute ProPR runtime components.
56 changes: 56 additions & 0 deletions apps/desktop/forge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { ForgeConfig } from '@electron-forge/shared-types';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { VitePlugin } from '@electron-forge/plugin-vite';
import { flipFuses, FuseV1Options, FuseVersion } from '@electron/fuses';
import { resolve } from 'node:path';

const config: ForgeConfig = {
packagerConfig: {
asar: true,
name: 'propr-desktop',
executableName: 'propr-desktop',
},
rebuildConfig: {},
hooks: {
packageAfterCopy: async (_forgeConfig, resourcesPath, _electronVersion, platform, arch) => {
const applePlatform = platform === 'darwin' || platform === 'mas';
const executableName = applePlatform ? 'Electron' : `electron${platform === 'win32' ? '.exe' : ''}`;
await flipFuses(resolve(resourcesPath, '..', '..', applePlatform ? 'MacOS' : '', executableName), {
version: FuseVersion.V1,
resetAdHocDarwinSignature: applePlatform && arch === 'arm64',
strictlyRequireAllFuses: true,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
[FuseV1Options.GrantFileProtocolExtraPrivileges]: false,
[FuseV1Options.WasmTrapHandlers]: true,
});
},
},
makers: [
new MakerSquirrel({ name: 'propr_desktop' }),
new MakerZIP({}, ['darwin', 'linux']),
...(process.env.PROPR_DESKTOP_ENABLE_DEB === '1' ? [new MakerDeb({})] : []),
...(process.env.PROPR_DESKTOP_ENABLE_RPM === '1' ? [new MakerRpm({})] : []),
],
plugins: [
new VitePlugin({
build: [
{ entry: 'src/main.ts', config: 'vite.main.config.ts' },
{ entry: 'src/preload.ts', config: 'vite.preload.config.ts' },
],
renderer: [
{ name: 'main_window', config: 'vite.renderer.config.ts' },
],
}),
],
};

export default config;
45 changes: 45 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@propr/desktop",
"productName": "ProPR Desktop",
"version": "0.8.15",
"private": true,
"description": "Secure ProPR desktop application",
"author": "Unchained Development OÜ / Rinalds Uzkalns",
"license": "Apache-2.0",
"homepage": "https://github.com/integry/propr",
"type": "module",
"main": ".vite/build/main.cjs",
"scripts": {
"prepare:renderer": "npm run build -w @propr/shared && npm run build -w @propr/client",
"predev": "npm run prepare:renderer",
"dev": "electron-forge start",
"pretypecheck": "npm run prepare:renderer",
"typecheck": "tsc --noEmit",
"test": "tsx --test src/**/*.test.ts",
"prepackage": "npm run prepare:renderer",
"package": "electron-forge package",
"smoke:package": "node scripts/smoke-packaged.mjs",
"premake": "npm run prepare:renderer",
"make": "electron-forge make",
"premake:deb": "npm run prepare:renderer",
"make:deb": "PROPR_DESKTOP_ENABLE_DEB=1 electron-forge make --targets @electron-forge/maker-deb",
"premake:rpm": "npm run prepare:renderer",
"make:rpm": "PROPR_DESKTOP_ENABLE_RPM=1 electron-forge make --targets @electron-forge/maker-rpm"
},
"devDependencies": {
"@electron-forge/cli": "8.0.0-alpha.10",
"@electron-forge/maker-deb": "8.0.0-alpha.10",
"@electron-forge/maker-rpm": "8.0.0-alpha.10",
"@electron-forge/maker-squirrel": "8.0.0-alpha.10",
"@electron-forge/maker-zip": "8.0.0-alpha.10",
"@electron-forge/plugin-vite": "8.0.0-alpha.10",
"@electron-forge/shared-types": "8.0.0-alpha.10",
"@electron/fuses": "^2.1.3",
"@types/node": "^22.10.0",
"@vitejs/plugin-react": "^4.6.0",
"electron": "^44.0.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.5"
}
}
17 changes: 17 additions & 0 deletions apps/desktop/renderer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https: http://127.0.0.1:* http://[::1]:* http://localhost:* ws://127.0.0.1:* ws://[::1]:* ws://localhost:* wss:; object-src 'none'; base-uri 'none'; form-action 'none'; frame-src 'none'"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#f8fafc" />
<title>ProPR Desktop</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="../../propr-ui/src/desktop.tsx"></script>
</body>
</html>
Loading
Loading