Skip to content

feat(native): mer native — WebView shell + window.mer.invoke bridge (P0–P3) - #100

Closed
pranavp311 wants to merge 86 commits into
justrach:release/v0.2.53from
pranavp311:feat/mer-native
Closed

feat(native): mer native — WebView shell + window.mer.invoke bridge (P0–P3)#100
pranavp311 wants to merge 86 commits into
justrach:release/v0.2.53from
pranavp311:feat/mer-native

Conversation

@pranavp311

@pranavp311 pranavp311 commented Jun 19, 2026

Copy link
Copy Markdown

Closes #101

Summary

Implements mer native for macOS: a Zig native shell hosting the system WebView (WKWebView) over the merjs loopback server, with a hardened window.mer.invoke() JS↔Zig bridge and manifest-driven .app packaging. No Electron, no bundled Chromium, no Node.

This follows the zero-native model. merjs already owns the server half (src/server.zig, routing, SSR, watcher, static assets); the native layer adds the WebView window, bridge, production gates, and packaging hooks.

PR #100 is now rebased/retargeted onto main. Linux (WebKitGTK) and Windows (WebView2) are planned and documented, but native runtime/package support remains macOS-only in this PR.

Developer experience

mer add native          # scaffold mer.app.zon + native/main.zig, print build.zig snippet
mer native              # dev: native window against the hot-reloading server
mer native build        # prod: build the native shell binary (ReleaseSmall)
mer package             # unsigned local .app bundle
mer package --sign      # codesign with hardened runtime
mer package --notarize  # notarytool submit + stapler
mer native doctor       # production manifest/security gate

mer native reuses the existing codegen/server/watcher pipeline and attaches a WebView window once the server reports its bound port. UI edits hot-reload inside the native window via the existing /_mer/events SSE channel.

What shipped

Native shell + CLI

New src/native/ module, re-exported as mer.native:

  • shell.zig — loopback server on port=0, ServerReady handshake, watcher/static-dir options, platform openWindow.
  • macos.zig — WKWebView + NSWindow via extern ObjC primitives, no @cImport.
  • manifest.zig — comptime parse/defaulting of mer.app.zon.
  • bridge.zig — hardened window.mer.invoke dispatch.
  • platform_commands.zig / macos_commands.zig — platform command facade and macOS command implementations.
  • update.zig — structural update feed/config validation helpers.
  • main.zig — native binary entry with --dev / --no-dev.

CLI/build additions:

  • mer native, mer native build, mer package, mer add native, mer native doctor.
  • zig build native, native-build, package, package-sign, package-notarize, native-prod-check, native-prod-release.
  • mer.app.zon + examples/starter/ templates.
  • CLI child processes now inherit the configured environment.

Hardened bridge/security model

Implemented bridge protections:

  • 64 KB payload limit.
  • Embedded-NUL guard before dispatch.
  • WKScriptMessage frame-origin validation in the macOS backend.
  • WKWebView navigation delegate cancellation for non-allowed origins.
  • Exact runtime loopback origin injection after the server binds.
  • Default extra navigation origins are empty.
  • Native server host is constrained to loopback IP literals (127.0.0.1 recommended; localhost intentionally rejected).
  • Deny-by-default command registry.
  • Manifest permission classes.
  • Explicit command allowlist via security.bridge.allowed_commands.
  • Per-command origin bindings via security.bridge.command_origins.
  • open.external scheme allowlist.
  • open.path fails closed unless explicit security.open.path_roots are configured.

Built-in commands:

  • mer.ping, mer.echo
  • clipboard.read, clipboard.write
  • dialog.openFile, dialog.pickDirectory, dialog.openDirectory
  • open.external, open.path
  • window.setTitle, window.close

Static custom command registry

Adds a safe static extension point without dynamic plugin loading:

  • Shell.RunOpts.commands
  • bridge.Ctx.extra_commands
  • bridge.dispatchWithRegistry(...)

Custom commands fail closed unless they:

  • use a valid app namespace;
  • avoid reserved built-in prefixes case-insensitively;
  • declare a non-empty permission;
  • are explicitly listed in allowed_commands;
  • have their permission granted in the manifest;
  • satisfy per-command origin bindings when configured.

Invalid custom registries resolve the caller with InvalidRegistry and invoke no built-in or custom handlers.

Packaging, signing, notarization, production gate

Packaging hardening:

  • Manifest-driven .app bundle / Info.plist.
  • XML escaping for plist strings.
  • Invalid UTF-8 / XML-invalid control-byte rejection.
  • Sanitized bundle path components.
  • CLI package path matches sanitized bundle path.
  • macOS-only guard for native/package commands.

Production/release hooks:

  • package-sign runs hardened-runtime codesign.
  • package-notarize submits via notarytool and staples.
  • native-prod-check fails closed unless manifest production hardening is present:
    • signing identity;
    • notarization profile;
    • loopback native host;
    • explicit navigation/bridge/open policies;
    • strict update metadata.

Update feed/config validation

Adds structural validation for future updater inputs:

  • provider allowlist: github-releases, custom-http;
  • strict HTTPS feed/artifact URLs;
  • ed25519:-tagged public key/signature fields;
  • lowercase SHA-256 artifact hashes;
  • positive artifact sizes;
  • unique (os, arch) platform entries;
  • numeric N[.N[.N]] versions;
  • min_supported_version <= version rollback-window validation;
  • manifest size cap.

This PR implements Ed25519 verification for signed update metadata plus artifact byte verification (size/SHA-256). It still does not perform automatic install/self-replacement; platform-specific installer and durable rollback storage remain deferred.

Server/runtime fixes requested during review

  • Request metadata is preserved before body reads.
  • SPA fallback no longer shadows real routes or framed request bodies.
  • Runtime uses Threaded IO on Zig 0.16-supported targets to avoid Linux std.Io.Evented/io_uring stdlib issues.

Linux/Windows platform planning

  • Added docs/native-platforms.md with staged Linux WebKitGTK and Windows WebView2 plan.
  • Added platform command facade so unsupported platforms fail closed without pretending runtime support exists.
  • Linux/Windows native runtime/package support remains deferred until platform SDK/runtime validation.

Verification

Final local validation on macOS / Zig 0.16.0 after latest push (1f52db5):

zig test src/native/update.zig                                      ✅
zig test src/native/bridge.zig -lc -framework AppKit -framework Foundation ✅
zig build test                                                      ✅
zig build cli                                                       ✅
zig build worker                                                    ✅
zig build wasm                                                      ✅
zig build prod                                                      ✅
zig build native-build -Doptimize=ReleaseSmall                       ✅
zig build package -Doptimize=ReleaseSmall                            ✅
git diff --check                                                    ✅
git status --short                                                  ✅ clean

Additional macOS validation from earlier commits:

zig build native-build -Doptimize=ReleaseSmall ✅
zig build package -Doptimize=ReleaseSmall      ✅
zig build native-prod-check                    ✅ fails closed on demo manifest missing production credentials/update metadata

Clean Linux evidence using Apple container on Debian bookworm / Zig 0.16.0 passed on branch commit 673d02f before the latest native-only hardening commit:

zig version      # 0.16.0
zig build test   ✅
zig build cli    ✅
zig build worker ✅
zig build wasm   ✅
zig build prod   ✅

Review / audit notes

Codebase-aware reviewer agents reviewed:

  • CLI env/test/artifact changes;
  • Linux/Windows platform prep;
  • updater/custom registry plan;
  • updater/custom registry implementation;
  • final implementation after security fixes.

A strict independent internal security-agent audit reviewed the updater/custom registry/security-hardening changes. Findings were fixed, including:

  • no default portless localhost origins;
  • update config validation wired into native startup;
  • open.path fail-closed defaults;
  • embedded-NUL rejection without attacker-chosen ids;
  • case-insensitive reserved command prefixes;
  • native server host constrained to loopback IP literals;
  • scaffold production gate drift fixed.

Final strict security re-audit found no remaining critical/high/medium blockers in the audited areas.

Current PR base / retarget note

This PR has been transplanted with git rebase --onto upstream/main origin/feat/mercss-responsive and retargeted to main. Range-diff review found the native series preserved, with follow-up fixes for main-only worker/WASM build compatibility.

Deferred / explicitly not claimed

Area Status Why deferred
Runtime auto-installer deferred Automatic install/self-replacement, atomic replacement, durable rollback storage, and UX need separate implementation and audit.
Linux native runtime deferred Needs WebKitGTK backend, message-origin extraction, linking, and Linux desktop validation.
Windows native runtime deferred Needs WebView2/Win32 backend, Windows path canonicalization, signing/package validation.
Dynamic plugins deferred Static custom commands are implemented; loading commands/plugins from disk remains out of scope.
Third-party security audit deferred Internal strict agent audit completed; external third-party audit is still process work.
Static/custom-scheme mode deferred Current prod path uses embedded loopback SSR; fully static mer://app remains separate.
Mobile C ABI deferred Requires iOS/Android-consumable artifacts and a larger API surface.
CEF / bundled Chromium deferred System WebView remains the target for this PR.

Notable files

src/native/               native shell, macOS backend, bridge, manifest, commands, update validation
build.zig                 native/package/sign/notarize/prod-check steps + hardening
cli.zig                   mer native/native build/package/add native + scaffold gates
docs/native.md            user docs
docs/native-production.md production release checklist
docs/native-platforms.md  Linux/Windows staged backend plan
SECURITY.md               native security model/status
src/server.zig            metadata preservation + SPA fallback hardening
src/runtime.zig           Zig 0.16 Threaded IO backend

@justrach

Copy link
Copy Markdown
Owner

🤝 Review + live-test handoff (for the next agent · cc @pranavp311)

Picked this up, reviewed it, and tested mer native end-to-end on Zig 0.16.0 / macOS. TL;DR: architecture is solid and the JS↔Zig bridge genuinely works — zig build test/native-build/package are all green, and I drove every bridge path live. There are 3 small things to fix before merge (all one-liners), plus a few nits.

✅ Verified working

  • zig build test (incl. the 7 bridge.zig tests), zig build native-build (5.8 MB binary), zig build package (→ MerJS.app) — all pass on 0.16.0.
  • Server-on-port=0 + ServerReady handshake binds an ephemeral port; cold start (launch→first byte) ≈ 55 ms.
  • SSR over loopback returns HTTP 200; served HTML is byte-1:1 with the page source.
  • Bridge proven live (the part the unit tests can't cover): built a demo page that fires window.mer.invoke() for all 5 outcomes on load. Captured all five arriving at Zig dispatch() with correct cmd/args/id — mer.ping, mer.echo (structured args round-tripped JS→NSString→UTF8→Zig), dialog.openFileHandlerError stub, clipboard.writePermissionDenied (after dropping clipboard from the manifest — the gate really blocks), and unknown→UnknownCommand. The whole ObjC chain (shim → postMessage → dynamically-allocated MerInvokeHandler → IMP → dispatchevaluateJavaScript) is exercised.

🔧 Issues to fix before merge

  1. Memory leak in dispatch() error pathssrc/native/bridge.zig (~L130 & L134, the two try quoteStr(...) args to resolveStr). quoteStr allocates, resolveStr copies it into a new allocation, and the quoteStr result is never freed. Leaks on every UnknownCommand and HandlerError — i.e. every dialog.openFile / clipboard.write call. The 7 tests miss it because they use an arena; reproduced under a leak-detecting allocator → "1 tests leaked memory." Fix: free the quoteStr result after resolveStr, or don't double-allocate.
  2. CLI prints the wrong .app pathcli.zig cmdPackage prints open zig-out/MerNative.app, but the bundle is named from display_nameMerJS.app. docs/native.md is already correct; only the print is stale, so the copy-paste hint 404s.
  3. mer add native scaffold snippet won't compile — the printed build.zig snippet calls addRoutesModule(b, native_mod, mer_mod), which is undefined in a user project (in-framework it's helpers.addRoutesModule(...) with a 6-arg signature). Pasting the snippet as instructed fails with "use of undeclared identifier".

📝 Nits (non-blocking)

  • Watcher path hardcoded to "app" in shell.zig — correct for a scaffolded project, but for the framework's own zig build native the routes live in examples/site/app, so hot-reload watches a nonexistent ./app. No crash.
  • WindowConfig defaults are dead — fromZon reads win.label/title/width/height directly, so a .zon window omitting any field is a comptime error instead of falling back to the struct default. Use @hasField like the server/permissions blocks.
  • Shutdown races — detached server/watcher threads touch runtime.io / the stack watcher after run() returns and defers fire on window close. Benign (process exits), inherited from the desktop spike, but worth a comment.

ℹ️ Not a code issue

Couldn't grab a screenshot of the native window from this environment — macOS screen-capture needs the host app's Accessibility/Screen-Recording grants, unrelated to the PR. Rendering is 1:1 regardless (WKWebView = Safari's WebKit; served SSR matches source byte-for-byte). For sanity I also stood up a real Next.js 15 app rendering the same UI: merjs native 5.8 MB / ~55 ms cold start / zero runtime vs Next.js ~322 MB on disk (node_modules + .next) / 773 ms Ready in / needs Node + a browser.

Handoff state: the 3 fixes are all one-liners; nothing pushed, all testing was in a throwaway worktree. Next agent: knock out 1–3, re-run zig build test + zig build package, and confirm the dialog/clipboard handlers are still expected to be post-v0.2.6 stubs (they are, per the PR description). 🚀

@pranavp311

Copy link
Copy Markdown
Author

Raised owner review/merge tracker: #101.

Latest PR branch push includes the blocker fixes from review:

  • complete native scaffold/build/package steps
  • bridge allowed-origin enforcement and safer Promise rejection paths
  • macOS app termination on last window close
  • v0.2.53 docs/manifest cleanup
  • release/** PR CI trigger

Local verification passed:

  • zig build test
  • zig build cli
  • zig build desktop
  • zig build native-build -Doptimize=ReleaseSmall
  • zig build package -Doptimize=ReleaseSmall
  • git diff --check

@justrach please review and merge PR #100 into release/v0.2.53 if the changes look good.

@pranavp311

Copy link
Copy Markdown
Author

Follow-up pushed for the non-blocking owner review nits: 55eb3bd.

Addressed:

  • Native watcher path is now manifest-driven via server.watch_dir, with the framework manifest watching examples/site/app and scaffolded apps defaulting to app.
  • WindowConfig defaults now apply when a window omits label/title/width/height; added manifest tests for this.
  • Added a shell lifecycle comment documenting the current detached server/watcher behavior and the future cooperative shutdown point.

Verification passed locally:

  • zig build test
  • zig build cli
  • zig build desktop
  • zig build native-build -Doptimize=ReleaseSmall
  • zig build package -Doptimize=ReleaseSmall
  • git diff --check

The only local unstaged file left is the pre-existing codedb.snapshot change, not included in the PR commits.

@pranavp311

Copy link
Copy Markdown
Author

Updated this PR with the native-shell side of the Cmd+W/window close fix in 23cca54 (pushed to feat/mer-native):

  • Added window.close to the built-in bridge registry.
  • Implemented macOS closeWindow() using AppKit performClose:.
  • Added File → Close Window with Cmd+W to the native macOS menu so the OS shortcut no longer bonks.

Validated in the MerJS repo:

  • zig build test

The Codegraff GUI-side usage/manifest changes are committed separately to the Codegraff PR (#84).

pranavp311 added a commit to pranavp311/merjs that referenced this pull request Jun 22, 2026
Issue/PR trajectory:

- Read the mer native epic and PR justrach#100 state against the local feat/mer-native branch. The feature is now macOS-first P0-P3 plus follow-up built-ins: manifest-driven WKWebView shell, ServerReady loopback binding, window.mer.invoke, clipboard/dialog/open/window commands, packaging, and docs.

- Reconciled owner/reviewer concerns with the local implementation instead of expanding scope: keep Linux/Windows/CEF/static mer://app/signing/custom registries deferred; keep PR justrach#100 focused on a safe macOS system-WebView release.

Security trajectory:

- Verified the shell already prepends the exact runtime loopback origin after port=0 binds, so portless manifest origins must not wildcard all 127.0.0.1 ports. Kept structured scheme/host/port matching and documented that runtime origin injection is what makes ephemeral binding work.

- Hardened the macOS WKScriptMessage path before bridge.dispatch: measure the full NSString UTF-8 byte length, reject oversized direct posts before C-string truncation, and reject embedded-NUL payloads that would otherwise let UTF8String/std.mem.span parse only a trusted prefix.

Docs trajectory:

- Updated docs/native.md to include the implemented window.close command and to describe the current frame-origin/global-origin policy accurately.

- Updated plans/mer-native.md so the plan matches the shipped PR surface: built-in registry plus top-level permissions/global origins now; bridge.commands per-command allowlists and app-level custom registries later; real built-ins instead of old dialog/clipboard stubs.

Validation trajectory:

- zig build test

- zig build cli

- zig build native-build -Doptimize=ReleaseSmall

- zig build package -Doptimize=ReleaseSmall

- git diff --check
pranavp311 added a commit to pranavp311/merjs that referenced this pull request Jun 27, 2026
Issue/PR trajectory:

- Read the mer native epic and PR justrach#100 state against the local feat/mer-native branch. The feature is now macOS-first P0-P3 plus follow-up built-ins: manifest-driven WKWebView shell, ServerReady loopback binding, window.mer.invoke, clipboard/dialog/open/window commands, packaging, and docs.

- Reconciled owner/reviewer concerns with the local implementation instead of expanding scope: keep Linux/Windows/CEF/static mer://app/signing/custom registries deferred; keep PR justrach#100 focused on a safe macOS system-WebView release.

Security trajectory:

- Verified the shell already prepends the exact runtime loopback origin after port=0 binds, so portless manifest origins must not wildcard all 127.0.0.1 ports. Kept structured scheme/host/port matching and documented that runtime origin injection is what makes ephemeral binding work.

- Hardened the macOS WKScriptMessage path before bridge.dispatch: measure the full NSString UTF-8 byte length, reject oversized direct posts before C-string truncation, and reject embedded-NUL payloads that would otherwise let UTF8String/std.mem.span parse only a trusted prefix.

Docs trajectory:

- Updated docs/native.md to include the implemented window.close command and to describe the current frame-origin/global-origin policy accurately.

- Updated plans/mer-native.md so the plan matches the shipped PR surface: built-in registry plus top-level permissions/global origins now; bridge.commands per-command allowlists and app-level custom registries later; real built-ins instead of old dialog/clipboard stubs.

Validation trajectory:

- zig build test

- zig build cli

- zig build native-build -Doptimize=ReleaseSmall

- zig build package -Doptimize=ReleaseSmall

- git diff --check
@pranavp311
pranavp311 changed the base branch from release/v0.2.53 to feat/mercss-responsive June 27, 2026 09:04
@justrach

justrach commented Jun 27, 2026

Copy link
Copy Markdown
Owner

@pranavp311 this native work is awesome - we built and launched zig-out/MerJS.app locally and it really does run fast. 🙌

Before we treat this as production-ready / merge-ready, can you add or split out a follow-up plan for the remaining native hardening pieces?

Requested additions:

  • code signing
  • notarization
  • auto-updater
  • full Linux support
  • full Windows support
  • hardened permission model
  • mature plugin system
  • production security audit

I looked through the current implementation with codedb/codedb-pro and here are the concrete places that seem like the right integration points:

Packaging / signing / notarization

Current macOS packaging lives in build.zig around the package step:

  • build.zig: native, native-build, package steps
  • build.zig: Info.plist generation and .app bundle assembly
  • cli.zig: cmdPackage, cmdNativeBuild, cmdNative

Suggested direction:

  • add zig build package-sign or flags/options for signing identity/team id
  • run codesign --deep --force --options runtime --timestamp ... zig-out/<App>.app
  • add notarization support via xcrun notarytool submit ... --wait and xcrun stapler staple ...
  • add manifest fields for signing metadata, e.g. .macos.signing_identity, .macos.team_id, .macos.entitlements

Auto-updater

There is currently no updater layer. Good places to hook it:

  • src/native/manifest.zig: extend manifest with update metadata
  • src/native/bridge.zig: add update-related commands if UI-triggered updates are desired
  • cli.zig: add packaging/build metadata emission

Possible manifest shape:

.update = .{
    .provider = "github-releases",
    .feed_url = "https://...",
    .public_key = "...",
}

Would be good to support signed update manifests/artifacts rather than downloading arbitrary binaries.

Linux + Windows support

Right now src/native/shell.zig explicitly guards to macOS only:

if (builtin.os.tag != .macos) return error.UnsupportedPlatform;

and dispatches only to:

@import("macos.zig").openWindow(...)

Current backend files:

  • src/native/macos.zig - AppKit/WKWebView backend
  • src/native/macos_commands.zig - clipboard/dialog/open/window native commands

Suggested additions:

  • src/native/linux.zig using WebKitGTK
  • src/native/linux_commands.zig
  • src/native/windows.zig using WebView2
  • src/native/windows_commands.zig
  • build/link logic in build.zig and generated native_build_snippet in cli.zig

Hardened permission model

Current bridge checks are a good start:

  • src/native/bridge.zig: dispatch() checks payload size, parses command envelope, enforces command permission
  • src/native/macos.zig: merInvokeIMP() checks frame origin before dispatch
  • src/native/manifest.zig: has global permissions and security.allowed_origins

Recommended hardening:

  • per-command allowlists in mer.app.zon, not only broad top-level permission strings
  • per-command origin restrictions, not only global origins
  • deny navigation to non-allowed origins in the WebView itself, not just bridge calls
  • path/url restrictions for open.path and open.external
  • stricter argument schemas per command
  • optional user prompts for sensitive permissions like clipboard/filesystem/dialog/open
  • tests for hostile origins, malformed payloads, oversized payloads, and command confusion

Plugin system

Current bridge registry is static in src/native/bridge.zig:

pub const registry = [_]Command{ ... };

That works for built-ins, but a mature plugin system probably needs:

  • app-provided command registries at comptime
  • plugin manifest/capabilities
  • permission declarations per plugin command
  • stable plugin API exported from src/native/mer.zig
  • tests/examples for a third-party plugin command

Production security audit

Would love a written checklist/doc before production claims. Areas to audit:

  • WKWebView navigation policy
  • bridge message origin validation
  • payload parsing / JSON schema validation
  • file/path/url handling in native commands
  • update signing / rollback prevention
  • macOS entitlements/sandbox assumptions
  • CSP interaction with injected window.mer shim
  • whether local loopback server endpoints expose anything sensitive

Also small PR cleanup before merge:

  • please remove accidental artifacts like harness.trajectory.jsonl
  • please avoid zeroing/changing codedb.snapshot unless intentional
  • ideally rebase/retarget this PR onto main so the diff only contains native work

Again, the core feature works locally and feels excellent - this is mostly the production-readiness checklist for making mer native something people can trust for real apps.

@justrach

Copy link
Copy Markdown
Owner

@pranavp311 I opened a tracking issue with the production-readiness checklist and concrete acceptance tests here: #103

@justrach

justrach commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Thanks for the native work here — I pulled the branch and did a local verification pass.

A few follow-ups before this is ready:

  1. I do not see codesigning / notarization / entitlements support in the branch yet. Packaging currently creates zig-out/MerJS.app with Info.plist + binary, but there are no codesign, hardened runtime, entitlements plist, or notarization/stapling steps. Can you either add those or explicitly split/document them as out-of-scope for this PR?

  2. Please add tests for the PR changes, especially:

    • native manifest parsing / defaults
    • bridge command permission + origin checks
    • CLI native/package paths, including inherited env propagation to child zig processes
    • static_dir / SPA fallback behavior not shadowing real routes
    • request metadata preservation across body reads
  3. Please rebase this PR on the latest main again after the above so we can retest cleanly.

During local testing I hit AppDataDirUnavailable from mer native build / mer package because child zig processes were not receiving the parent env (HOME, cache dirs, etc.). I have a local patch for that if helpful, but it should be covered by tests in this PR.

@justrach

Copy link
Copy Markdown
Owner

One more verification request for the Linux side:

Please use apple/container to run the relevant Linux build/test path in a clean container and paste the command + output log into this PR as evidence.

Suggested evidence to include:

# from a clean container / clean checkout
zig version
zig build test
zig build cli
zig build worker
zig build wasm
zig build prod

If native macOS packaging is intentionally macOS-only, that’s fine — the goal here is to show the non-native framework/CLI paths still work cleanly on Linux after the native additions, and to have the container log in the PR for review/release confidence.

@pranavp311

Copy link
Copy Markdown
Author

Follow-up pushed in 40f2f6a for the latest owner feedback.

What changed:

  • CLI child zig invocations now inherit the parent process environment through shared runInheritEnv / spawnWaitInheritEnv helpers, covering mer native, mer native build, mer native doctor, mer package, mer build, mer update, mer dev, mer init, and Tailwind download paths.
  • Added a behavioral CLI env propagation test using a synthetic env and child env process.
  • Wired the CLI test module with the runtime import so inline CLI tests run under zig build test.
  • Added request-target metadata and SPA fallback pure helpers/tests so query/path metadata is split before body reads and static SPA fallback cannot shadow real routes or framed-body requests.
  • Removed the tracked harness.trajectory.jsonl artifact and updated .gitignore to keep harness/session/codedb handoff artifacts local-only.

Validation on macOS / Zig 0.16.0:

zig version
# 0.16.0

zig build test
zig build cli
zig build worker
zig build wasm
zig build prod
zig build native-build -Doptimize=ReleaseSmall
zig build package -Doptimize=ReleaseSmall
git diff --check

All passed locally.

Also verified the production gate still fails closed on the demo manifest, as expected, with missing signing/notary/update metadata:

zig build native-prod-check
# exits 1 with missing .macos.signing_identity, .macos.notarization_profile,
# .update.provider, .update.feed_url, .update.public_key

I also had an isolated reviewer agent inspect the uncommitted diff before commit; it found no production-readiness blockers.

Remaining external evidence: I do not have apple/container installed in this local harness (command -v container returned empty), so the requested clean Linux apple/container transcript still needs to be run from a host with that tool available.

@pranavp311

Copy link
Copy Markdown
Author

Added a Linux/Windows native-platform planning + safe Phase 1 prep commit: 5290d6e.

Scope deliberately stays conservative: it does not claim Linux/Windows runtime support yet. It prepares the shared code so those backends can be added without weakening the macOS bridge/security model.

What changed:

  • Added docs/native-platforms.md with the staged Linux WebKitGTK and Windows WebView2 implementation plan, backend milestones, security invariants, packaging gates, and validation expectations.
  • Added src/native/platform_commands.zig, a command facade that selects macOS command implementations today and fail-closed unsupported stubs on planned platforms.
  • Refactored bridge.zig to call the platform command facade instead of importing macOS commands directly.
  • Kept mer native / mer package macOS-only for now, with clearer Linux WebKitGTK / Windows WebView2 planned-backend messages.
  • Documented that Linux/Windows production packaging/signing/runtime checks will be platform-specific follow-ups.
  • Fixed target-aware native bridge test linking so macOS frameworks are only linked when the target is macOS, not merely when the build host is macOS.
  • Added a Windows fail-closed path canonicalizer so the platform-neutral bridge compiles for Windows without pulling POSIX realpath/libc.

Validation passed locally:

zig build test
zig build cli
zig build worker
zig build wasm
zig build prod
zig build native-build -Doptimize=ReleaseSmall
zig build package -Doptimize=ReleaseSmall
zig test src/native/platform_commands.zig -target x86_64-linux --test-no-exec
zig test src/native/platform_commands.zig -target x86_64-windows --test-no-exec
zig test src/native/bridge.zig -target x86_64-linux --test-no-exec -lc
zig test src/native/bridge.zig -target x86_64-windows --test-no-exec
git diff --check

I also had a thorough reviewer agent inspect this platform-prep diff. Initial blockers were found around Windows bridge compile assumptions and host-vs-target framework linking; both were fixed and the final review found no blockers.

@pranavp311

Copy link
Copy Markdown
Author

Clean Linux evidence using apple/container is now available.

Host/tooling:

container --version
# container CLI version 1.0.0 (build: release, commit: ee848e3)
container system status
# status: running

Container command used from macOS host:

container run --rm -i --memory 8G \
  --mount type=bind,source=/tmp/container-mount,target=/mnt/host,readonly \
  docker.io/library/debian:bookworm bash -s <<'SH'
set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends ca-certificates git xz-utils build-essential libc-dev
mkdir -p /opt/zig
tar -xJf /mnt/host/zig-aarch64-linux-0.16.0.tar.xz -C /opt/zig --strip-components=1
export PATH=/opt/zig:$PATH
mkdir -p /work
cd /work
git clone --depth 1 --branch feat/mer-native https://github.com/pranavp311/merjs.git merjs
cd merjs
git rev-parse --short HEAD
zig version
zig build test
zig build cli
zig build worker
zig build wasm
zig build prod
SH

Relevant output:

+ git clone --depth 1 --branch feat/mer-native https://github.com/pranavp311/merjs.git merjs
Cloning into 'merjs'...
+ cd merjs
+ git rev-parse --short HEAD
673d02f
+ zig version
0.16.0
+ zig build test
codegen: wrote 19 route(s) to src/generated/routes.zig
mercss: wrote 693 bytes (1019 candidates, 20 sources) to app/_mercss.css
+ zig build cli
+ zig build worker
codegen: wrote 19 route(s) to src/generated/routes.zig
mercss: wrote 693 bytes (1019 candidates, 20 sources) to app/_mercss.css
+ zig build wasm
+ zig build prod
codegen: wrote 19 route(s) to src/generated/routes.zig
mercss: wrote 693 bytes (1019 candidates, 20 sources) to app/_mercss.css
info(prerender): /about → dist/about.html (6135 bytes)
info(prerender): 1 page(s) pre-rendered, 18 skipped (SSR-only)

While getting this running, the first clean Linux pass exposed a Zig 0.16 std.Io.Evented/io_uring stdlib compile issue on Linux (ReadOnlyFileSystem error-set mismatch inside std/Io/Uring.zig). I pushed efd2db4 / 673d02f to keep merjs on std.Io.Threaded for Zig 0.16-supported targets until upstream Evented is fixed. That is included in the clean Linux evidence above.

@pranavp311

Copy link
Copy Markdown
Author

Latest native hardening update pushed in f11afdf and PR body updated.

What changed in this follow-up:

  • Added src/native/update.zig with structural update feed/config validation:
    • provider allowlist (github-releases, custom-http)
    • strict HTTPS feed/artifact URLs
    • ed25519:-tagged public key/signature fields
    • lowercase SHA-256 artifact hashes
    • positive artifact sizes
    • unique (os, arch) platform entries
    • numeric versions and rollback-window validation
  • Added static custom command registry support:
    • Shell.RunOpts.commands
    • bridge.Ctx.extra_commands
    • bridge.dispatchWithRegistry(...)
    • custom commands require explicit allowlist, non-empty permission, origin checks, and valid app namespace
  • Additional hardening from strict security review:
    • default extra navigation origins are empty
    • exact runtime origin is injected by the shell
    • native server host is constrained to loopback IP literals (127.0.0.1 recommended; localhost intentionally rejected)
    • open.path fails closed unless explicit roots are configured
    • embedded-NUL direct bridge posts reject without resolving attacker-chosen ids
    • reserved built-in command prefixes are checked case-insensitively
    • production gates/scaffold gates now validate strict update metadata

Important scope note: this still does not claim runtime auto-update download/install or Ed25519 cryptographic verification. This PR adds the fail-closed structural contract and docs for future updater runtime work.

Final validation after push on macOS / Zig 0.16.0:

zig test src/native/update.zig                                      ✅
zig test src/native/bridge.zig -lc -framework AppKit -framework Foundation ✅
zig build test                                                      ✅
zig build cli                                                       ✅
zig build worker                                                    ✅
zig build wasm                                                      ✅
zig build prod                                                      ✅
git diff --check                                                    ✅
git status --short                                                  ✅ clean

Review/audit summary:

  • Plan reviewed by a reviewer agent before implementation.
  • Implementation reviewed by a reviewer agent after implementation and after fixes.
  • Strict internal security-agent audit was run on updater/custom-command/native hardening changes.
  • Audit findings were fixed and final strict re-audit reported no remaining critical/high/medium blockers in the audited areas.

pranavp311 added a commit to pranavp311/merjs that referenced this pull request Jun 29, 2026
Issue/PR trajectory:

- Read the mer native epic and PR justrach#100 state against the local feat/mer-native branch. The feature is now macOS-first P0-P3 plus follow-up built-ins: manifest-driven WKWebView shell, ServerReady loopback binding, window.mer.invoke, clipboard/dialog/open/window commands, packaging, and docs.

- Reconciled owner/reviewer concerns with the local implementation instead of expanding scope: keep Linux/Windows/CEF/static mer://app/signing/custom registries deferred; keep PR justrach#100 focused on a safe macOS system-WebView release.

Security trajectory:

- Verified the shell already prepends the exact runtime loopback origin after port=0 binds, so portless manifest origins must not wildcard all 127.0.0.1 ports. Kept structured scheme/host/port matching and documented that runtime origin injection is what makes ephemeral binding work.

- Hardened the macOS WKScriptMessage path before bridge.dispatch: measure the full NSString UTF-8 byte length, reject oversized direct posts before C-string truncation, and reject embedded-NUL payloads that would otherwise let UTF8String/std.mem.span parse only a trusted prefix.

Docs trajectory:

- Updated docs/native.md to include the implemented window.close command and to describe the current frame-origin/global-origin policy accurately.

- Updated plans/mer-native.md so the plan matches the shipped PR surface: built-in registry plus top-level permissions/global origins now; bridge.commands per-command allowlists and app-level custom registries later; real built-ins instead of old dialog/clipboard stubs.

Validation trajectory:

- zig build test

- zig build cli

- zig build native-build -Doptimize=ReleaseSmall

- zig build package -Doptimize=ReleaseSmall

- git diff --check
@pranavp311
pranavp311 changed the base branch from feat/mercss-responsive to main June 29, 2026 09:09
@pranavp311

Copy link
Copy Markdown
Author

Rebase / updater / audit follow-up is pushed and PR #100 is now retargeted to main.

Latest head: 1f52db5

What changed:

  • Rebased with the safe transplant form:
    git rebase --onto upstream/main origin/feat/mercss-responsive rebase/feat-mer-native-main
    then force-pushed feat/mer-native and changed the PR base to main.
  • Range-diff review showed the native series preserved; one stale stacked mercss-jit test reference was removed.
  • Added main-only worker/WASM compatibility fixes so the full requested non-native path still builds on the rebased branch.
  • Updater moved beyond structural validation:
    • Ed25519 public key validation (ed25519:<base64 raw 32-byte public key>)
    • Ed25519 signatures over canonical length-prefixed metadata payloads
    • signed metadata_version anti-replay state
    • signed min_supported_version, notes_url, published_at, app/version/platform/url/hash/size
    • artifact byte verification against signed size + SHA-256
    • no automatic install/self-replacement yet; that remains intentionally deferred
  • Ran one more independent strict security subagent audit. Findings were fixed through several iterations; final audit reported no critical/high/medium updater or bridge blockers.

Validation on macOS / Zig 0.16.0 after rebase to main:

zig test src/native/update.zig                                      ✅
zig build test                                                      ✅
zig build cli                                                       ✅
zig build worker                                                    ✅
zig build wasm                                                      ✅
zig build prod                                                      ✅
zig build native-build -Doptimize=ReleaseSmall                      ✅
zig build package -Doptimize=ReleaseSmall                           ✅
git diff --check                                                    ✅
git status --short                                                  ✅ clean

PR state after retarget:

base: main
head: 1f52db5
mergeStateStatus: CLEAN

@pranavp311

Copy link
Copy Markdown
Author

Added and pushed a zero-trust-oriented native bridge hardening pass in a1366ce:

  • per-process 256-bit bridge capability generated by the native shell using runtime.io.randomSecure;
  • bridge tokens required by default before command lookup/handler execution;
  • invalid token/origin failures no longer resolve attacker-selected callback ids;
  • native responses echo the token before the JS shim resolves promises;
  • window.mer is frozen/non-writable, falsy args are preserved, commands are validated client-side, and payload size is checked by UTF-8 byte length before posting;
  • lower-level macOS bridge setup fails closed without a valid token;
  • plain zig test src/native/bridge.zig now avoids linking ObjC/AppKit by using fail-closed native command stubs in test mode;
  • added docs/native-zero-trust.md and updated native/security docs with residual zero-trust maturity gaps.

Independent reviewer subagents audited the token/origin model; final focused review found no high/medium defects.

Validation on macOS / Zig 0.16.0:

zig test src/native/bridge.zig
# 29 passed; 1 skipped; 0 failed

zig build test
zig build cli
zig build worker
zig build wasm
zig build prod
zig build native-build -Doptimize=ReleaseSmall
zig build package -Doptimize=ReleaseSmall
git diff --check

All passed locally. I restored regenerated tracked worker .wasm artifacts after validation so this commit only contains the hardening/docs changes.

Positioning: mer native is now more zero-trust-oriented, fail-closed, and least-agency by default for bridge dispatch; it is not claiming fully mature/certified zero trust yet.

Repository owner deleted a comment from codegraff-bot Bot Jun 30, 2026
Repository owner deleted a comment from codegraff-bot Bot Jun 30, 2026
@pranavp311

Copy link
Copy Markdown
Author

Addressed the latest PR #100 readiness feedback and pushed 1779124 (address native PR readiness feedback).

Plan executed for all reported items (including nice-to-fix):

  • Hardened open.external beyond scheme checks: rejects controls/backslashes, userinfo, malformed HTTP(S), bad ports, invalid bracketed IPv6, and native-ambiguous host characters.
  • Moved global origin enforcement into bridge dispatch as a backstop for lower-level embedders.
  • Closed the portless command_origins footgun: portless command-origin entries now require a global exact-origin allowlist; otherwise embedders must specify exact ports.
  • Validate custom handler JSON fragments before embedding them in native JS resolver calls.
  • Package the configured static directory into Contents/Resources/<static_dir>/ and make packaged apps serve that resource path instead of falling back to CWD-controlled assets.
  • Added safe relative server.static_dir validation in root build, generated native snippet, and runtime fallback logic.
  • Split package-sign from the full production gate: signing-only now requires signing identity (entitlements optional), while package-notarize / native-prod-release still require the full production manifest gate.
  • Allow empty extra navigation origins in production manifests; shell-injected exact runtime origin remains the default trust boundary.
  • Updated native/security/zero-trust docs and manifest examples accordingly.
  • Broadened local handoff ignore pattern to avoid PR handoff artifacts.

Reviews:

  • Ran reviewer/skeptic agents over the implementation.
  • Initial review found issues around static resource fallback and path validation; fixed those.
  • Final focused reviews reported no high/medium blockers.

Validation on macOS / Zig 0.16.0:

zig test src/native/bridge.zig
zig test src/native/manifest.zig
zig test src/native/update.zig
zig build test
zig build cli
zig build worker
zig build wasm
zig build prod
zig build native-build -Doptimize=ReleaseSmall
zig build package -Doptimize=ReleaseSmall
zig test src/native/platform_commands.zig -target x86_64-linux --test-no-exec
zig test src/native/platform_commands.zig -target x86_64-windows --test-no-exec
zig test src/native/bridge.zig -target x86_64-linux --test-no-exec -lc
zig test src/native/bridge.zig -target x86_64-windows --test-no-exec
git diff --check

All passed locally. Confirmed zig-out/MerJS.app/Contents/Resources/public/.gitkeep exists after package.

Additional expected checks:

zig build package-sign -Doptimize=ReleaseSmall
# fails with only missing signing identity, not the full production gate

zig build native-prod-check
# intentionally fails for the demo/dev manifest because real signing/notary/update trust-root metadata is not configured

PR readiness review:

  • Feature/dev merge posture: looks ready from local validation and final agent review.
  • Production-release posture: still intentionally gated until real .macos.signing_identity, .macos.notarization_profile, and .update.* trust-root metadata are supplied.
  • GitHub currently reports mergeStateStatus: UNSTABLE with no status checks in statusCheckRollup; owner/CI should still confirm before merge.

@justrach

Copy link
Copy Markdown
Owner

@codegraff-bot review?

@codegraff-bot

codegraff-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

On it — I picked up:

review?

Running on your Codegraff account — I'll post my review here when it's done.

pranavp311 and others added 25 commits August 4, 2026 14:01
Keep the complete interactive utility demonstration in one page-sized exception because splitting markup sections would not yield independently useful behavior.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Add semantic page structure separately from its generated design stylesheet so content and presentation can be reviewed independently.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Add the cohesive generated dashboard stylesheet as a CSS-file exception; arbitrary rule splitting would make visual review harder, not easier.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Add the versioned release route and semantic markup before its isolated presentation layer.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Keep the versioned generated stylesheet intact as a CSS-file exception so cascade and responsive behavior remain reviewable together.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Separate budget policy and host-fetch bridging from the main Worker so their validation and resource bounds are focused.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Apply bounded request, AI, and streaming behavior in the deployment entry point as a single-file exception because they share cancellation state.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Commit the required npm lockfileVersion 3 separately from source to pin Wrangler 4.118.0 and transitive deployment tools; regenerate it with npm install in examples/site/worker/worker.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Wire Worker package metadata and Wrangler configuration only after runtime and bridge modules are complete.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Remove checked-in build outputs from site and Worker examples so clean builds prove every required artifact dependency.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Update component helpers, routes, and build wiring together because the showcase is a compact consumer-level compatibility fixture.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Update the Vercel adapter, configuration, documentation, and matching runtime artifact together because deployment tests consume the bundled WASM contract.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Harden the primary and package-facing shell installers together so platform selection, download, and extraction follow one release contract.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Add hermetic installer and release-version checks separately from implementation so failure cases remain explicit review evidence.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Update the documentation and Cloudflare-hosted installer after the canonical installer contract is fixed, keeping each distribution path consistent.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Cover clean deployment assembly and generated snapshot behavior together because both validate the packaged Worker artifact graph.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Exercise bridge cancellation, bounds, and response semantics in a focused test module independent of deployment assembly.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Remove superseded browser fixtures now that deployment and bridge tests cover the maintained runtime paths.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Explain utility syntax, Tailwind mappings, and Next.js migration together so users can evaluate the complete CSS compatibility surface.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Document the native application model and manifest workflow as one review-sized user guide.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Keep platform support and zero-trust guidance together because capability differences determine the exposed security boundary.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Explain signing, notarization, updates, and durable anti-replay state independently from feature planning so release owners have an actionable runbook.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Retain the native design plan separately from user documentation because it records scope and trade-offs rather than operational instructions.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Refresh public primitives, architecture, README, and security policy after all runtime surfaces are finalized.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
Update Linux, macOS, npm, PyPI, beta, and release workflows together because they enforce one cross-platform publication contract and remain within the review-size guideline.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
@pranavp311

Copy link
Copy Markdown
Author

History reconstruction complete

I rewrote feat/mer-native from the PR base (fe5b35c) so the feature can be reviewed as dependency-ordered, subsystem-focused commits rather than large review/fix bundles.

What changed

  • Replaced the previous 100-commit graph with 86 linear commits.
  • Split runtime, HTTP, merCSS, Worker, native, auth, packaging, examples, tests, CI, and documentation into focused review units.
  • Split the native bridge evolution into independently testable commits and ordered leaf modules before facades/public exports.
  • Preserved a remote safety branch: backup/feat-mer-native-before-history-rewrite-4d2a6e8.

Size profile

  • 61/86 commits are at or below 400 changed text lines.
  • 20 are 401–999 lines and are predominantly cohesive single-file modules.
  • Four are 1,000–1,537-line single-file/generated exceptions: the HTTP server, merCSS JIT, CLI, and Worker lockfile.
  • One explicit mechanical exception deletes 138,223 lines of stale zig-pkg/ vendor cache.
  • The lockfile is isolated and documents its npm install regeneration path; binary/WASM removals are isolated as artifacts.

Why this structure

The approximate 400-line target is applied as a reviewability guideline, not by creating syntactically invalid partial Zig files. Cohesive state machines/protocol modules remain atomic where arbitrary hunk splitting would make commits non-buildable or misleading.

Verification

  • New head: ce5c38b13654e8942aed2f6a060dafa0d84622ae
  • Final tree exactly matches old head 4d2a6e8 (b4d4f85cf4c832c918379d3509a6aaf961ad8715).
  • All commit messages have rationale and the required co-author trailer; every commit passes diff-tree --check.
  • Targeted corrected boundaries pass zig build test; every native bridge milestone passes direct zig test src/native/bridge.zig.
  • Full suite: 301 passed, 3 skipped.
  • Passed: formatting/diff checks, zig build prod, zig build cli, zig build package -Doptimize=ReleaseSmall.
  • Passed clean archive probes: Worker-only asset build and credential-free package-release-gated production graph.

GitHub still shows no hosted checks or approving review on the rewritten head, so workflow approval/execution and maintainer review remain merge gates.

@pranavp311

Copy link
Copy Markdown
Author

Superseded by #107, a clean restart from release/v0.2.53. The replacement keeps the first macOS runtime slice to 8 files / 499 changed lines, with every commit below 400 lines. This PR remains available as design and follow-up reference; its pre-rewrite tree is also preserved on backup/feat-mer-native-before-history-rewrite-4d2a6e8.

@pranavp311 pranavp311 closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Review and merge PR #100 for release/v0.2.53

2 participants