Skip to content

Protected Registry Desktop App

This note captures the current npm registry-related FractalOps surface and the new protected package delivery plan.

Build a desktop application with an embedded CLI surface for protected package install, publish, build hardening, and customer delivery.

The app is not a standalone npm registry. It is a FractalOps client:

  • browser access uses OIDC through Pomerium and Keycloak
  • package install/publish uses FractalOps-issued short-lived npm tokens
  • every token mint is checked against SCIM-projected entitlement
  • package consumers receive hardened package artifacts, not raw source
  • customer delivery stores protected artifacts inside the project delivery area

FractalOps already has the core registry base:

  • fractalops/backend/src/fractalops/contexts/access/application/integration/registry_login.py mints Nexus npm tokens and renders .npmrc.
  • fractalops/backend/src/fractalops/cli.py exposes fractalops registry login and fractalops registry login-machine.
  • fractalops/backend/src/fractalops/contexts/access/application/integration/credential_broker.py brokers nexus credentials with scopes such as install, publish, and admin.
  • fractalops/ops/lxc/assets/nexus-pomerium-plugin/index.js maps Pomerium identity, registry JWTs, and service-token identities into Nexus access.
  • fractalops/ops/containers/nexus/ builds/releases the Nexus image.
  • fractalops/platform/k8s/apps/nexus/ owns the Kubernetes deployment.
  • fractalops-frontend/portal/src/components/organisms/ProjectPackageHub.astro exposes package creation, install-token minting, publish-token minting, .npmrc copy, and registry launch.

Existing model:

principal
-> FractalOps session / Pomerium claims / service token
-> group_paths and policy roles
-> RegistryTokenService
-> short-lived npm token
-> .npmrc
-> Nexus install/publish

Current token minting uses projected group_paths. The protected package delivery plan must make the entitlement gate explicit:

OIDC identity
-> SCIM-projected group and project entitlement
-> FractalOps credential policy
-> registry token with package/project/action claims
-> Nexus plugin validation

Required token claims:

  • tenant_id
  • project_slug
  • package_name
  • package_scope
  • allowed_actions: install, publish, or both
  • entitlement_snapshot_id
  • artifact_policy: hardened, encrypted_delivery, or both
  • exp

The Nexus plugin must reject mismatched package/action usage even if the token is otherwise valid.

Implemented first backend enforcement slice:

  • RegistryTokenService.exchange_token() now emits protected registry claims: tenant_id, project_slug, package_name, package_scope, allowed_actions, entitlement_snapshot_id, artifact_policy, and exp.
  • Protected token minting now requires explicit project entitlement when project_slug, package_name, or action_mode is present.
  • PortalCredentialBrokerService.mint() passes tenant/project/package/action context into Nexus npm token issuance.
  • POST /v1/packages/registry/tokens/npm exposes a desktop-app-facing token mint path through the existing credential broker and audit grant pipeline.
  • The Nexus Pomerium plugin maps registry token claims into internal claim groups and rejects install/publish when the requested package, package scope, or action does not match the token.
  • apps/protected-registry-desktop/ now contains the first desktop-app command layer:
    • protected-registry install <package> --project <slug>
    • protected-registry publish <package> --project <slug>
    • a FractalOps token client for POST /v1/packages/registry/tokens/npm
    • temp .npmrc creation with cleanup in finally
    • npm, pnpm, and yarn runner support using per-command userconfig
  • Protected package lifecycle endpoints now exist:
    • POST /v1/packages/protection/builds
    • POST /v1/packages/delivery/bundles
    • GET /v1/packages/delivery/bundles/{bundle_id}
    • GET /v1/packages/delivery/view
    • POST /v1/packages/delivery/grants
  • Protection build requests record the hardening pipeline contract as audit and evidence metadata.
  • Delivery bundle requests store key_ref only and reject raw key values.
  • Delivery grants record customer-specific access grants as audit and evidence metadata.
  • The delivery view read model lists project/customer/package bundles, grants, and evidence record URLs from the existing audit/evidence resources.
  • Desktop app local executors now include:
    • protected-registry protect-build <package> --project <slug> --version <version> --source <path>
    • protected-registry deliver <package> --project <slug> --customer <slug> --version <version> --artifact <path> --key-ref <uri>
    • protected build manifest, integrity, SBOM, provenance, and private source-map escrow metadata writers
    • dependency-free JavaScript hardener that removes public source-map pointers, strips comments/whitespace, mangles declaration identifiers, and writes a protected artifact tree separate from raw source
    • production hardening package selection is now explicit: terser and javascript-obfuscator are optional desktop dependencies for production builds, with builtin-js-hardener as a recorded fallback
    • AES-256-GCM delivery bundle writer that reads the encryption key from the local process only and writes key_ref to the manifest
    • delivery key provider descriptors for openbao://, aws-kms://, gcp-kms://, and azure-keyvault:// references; manifests record provider and reference_only policy without raw key material
    • OpenBao Transit resolver support for provider-backed bundle encryption; it sends ciphertext to /v1/transit/decrypt/<key> and never writes Vault token or unwrapped key material into manifests
    • OpenBao grant materializer support for grant-delivery --materialize-grant; it writes a Transit decrypt ACL policy first, then sends key_grant_ref and grant_materialization metadata to FractalOps
    • cloud KMS request boundaries for aws-kms://, gcp-kms://, and azure-keyvault:// refs so provider SDK clients can be plugged in without changing bundle manifests
    • cloud KMS grant materializer adapters for AWS CreateGrant, GCP KMS IAM binding writes, and Azure Key Vault access-policy writes; adapters return key_grant_ref and grant_materialization metadata without provider credentials or unwrapped key material
    • protected-registry submit-bundle --manifest <path> to submit encrypted delivery manifests to FractalOps
    • protected-registry grant-delivery --bundle-id <id> --customer <slug> --grantee <subject> to request customer-specific delivery grants
    • FractalOps package lifecycle client for delivery bundle and grant APIs
  • Desktop auth/session modules now include:
    • OIDC login URL generation for the desktop shell
    • OIDC callback parsing with state validation
    • loopback callback receiver for browser/desktop login completion
    • public session view that redacts token values
    • Windows DPAPI-backed secure store adapter for user-scoped session storage
    • protected-registry login, complete-login, session, and logout command surface
    • protected-registry login --listen for local callback capture
    • protected package commands require an authenticated session vault entry with an access token before install, publish, protect-build, deliver, submit-bundle, or grant-delivery can run
    • install/publish token minting reads the OIDC access token from the session vault instead of accepting environment or CLI access-token bypasses
  • A first static UI shell exists under apps/protected-registry-desktop/src/ui/ with package, build, delivery, and session surfaces. It is intentionally token-free, renders a login gate as the first screen, and delegates privileged actions to the command/session modules.
  • Tauri host scaffolding now exists under apps/protected-registry-desktop/src-tauri/:
    • tauri.conf.json loads the static UI shell from ../src/ui
    • Windows package targets are msi and nsis
    • Rust host exposes a minimal command hook for secure-store UI integration
    • package scripts expose doctor, tauri:dev, and tauri:build
  • A native build readiness doctor exists at apps/protected-registry-desktop/src/doctor/build-readiness.mjs; it checks Node, Cargo, Tauri config, Rust host, desktop CLI bin wiring, and production hardening package selection before a native build attempt.
  • Desktop verification scripts now exist:
    • npm run verify runs desktop tests and includes the native readiness report without failing the whole check when Cargo is missing.
    • npm run verify:native runs the same checks in strict native mode and fails until Cargo/Tauri native readiness is complete.
    • npm run verify:hardening imports terser and javascript-obfuscator, verifies their runtime APIs, and fails if the production build environment would fall back to builtin-js-hardener.
    • npm run verify:native-artifacts checks that a Tauri build produced both MSI and NSIS installer artifacts under src-tauri/target/release/bundle, computes SHA-256 for each installer, and writes native-artifacts.json.
    • From the repo root, use pnpm --dir apps/protected-registry-desktop verify or pnpm run verify:protected-registry-desktop.
  • .github/workflows/protected-registry-desktop.yml now gates this package:
    • Ubuntu contract job runs pnpm run verify:protected-registry-desktop.
    • Windows native job installs Rust stable, runs pnpm run verify:protected-registry-desktop:native, executes cargo check and cargo test for the Tauri host, verifies production hardening packages, runs pnpm --dir apps/protected-registry-desktop tauri:build, and smoke tests the built executable.
    • The native job then runs verify:native-artifacts and uploads native-artifacts.json with MSI/NSIS installers as the protected-registry-desktop-windows artifact.
  • The static UI shell now has a Tauri bridge:
    • the first screen is login-only until the host reports an authenticated session
    • the login screen exposes only Login with FractalOps; OIDC issuer, client, redirect, and callback details are app-owned configuration, not user-facing fields
    • FractalOps/OIDC endpoints come from the repository root .env or build env: FRACTALOPS_BASE_URL for FractalOps API calls, PROTECTED_REGISTRY_OIDC_ISSUER_URL, PROTECTED_REGISTRY_OIDC_CLIENT_ID, and PROTECTED_REGISTRY_OIDC_REDIRECT_URI for login; the build also accepts FRACTALOPS_PORTAL_PUBLIC_URL as a legacy portal URL fallback
    • the native host opens the FractalOps authorization URL in the browser and uses a local loopback callback listener to save the completed session back into the desktop app without manual callback pasting
    • package actions remain hidden and disabled before authentication
    • session_storage_label returns the OS secure-store label to the UI
    • protected_registry_session returns a token-free public session status to the UI
    • complete_protected_registry_login stores the completed session through the desktop CLI/session vault boundary
    • queue_protected_registry_command accepts package command intent without token fields
    • UI offline fallback still works outside Tauri for browser inspection/tests
  • The Rust host now maps queued package intents into an explicit protected-registry CLI command spec with argv and session-vault-only token handling, so the privileged native boundary is defined without exposing access tokens in the UI.
  • The Rust host also exposes run_protected_registry_command, which validates package/build/delivery intents and executes protected-registry without a shell. It removes portal access-token and delivery-key environment variables before launch so commands must use the session vault or explicit secure provider flow.
  • Rust host unit tests cover native command argv mapping, delivery required fields, unsupported command rejection, blocked secret environment keys, and a Windows mock CLI execution that proves secret env values are stripped before process launch.
  • Native execution resolves the CLI through PROTECTED_REGISTRY_CLI_PATH first and falls back to protected-registry on PATH; the doctor reports this as desktop-cli-runtime-path for built-app verification.

Still pending:

  • Windows native CI packaging evidence still needs to be collected and attached to the branch once CI runs.

Current local native evidence:

  • cargo check --manifest-path apps/protected-registry-desktop/src-tauri/Cargo.toml passes on Windows with Cargo 1.96.0.
  • cargo test --manifest-path apps/protected-registry-desktop/src-tauri/Cargo.toml passes 5 Rust host tests.
  • pnpm --dir apps/protected-registry-desktop verify:native passes 47 desktop tests plus strict native readiness.
  • pnpm --dir apps/protected-registry-desktop tauri:build produced both installers:
    • MSI: src-tauri/target/release/bundle/msi/Protected Registry_0.1.0_x64_en-US.msi (sha256=b96943dd6a8c1015be1bc8056ea35fdbd7e76929eee51d0cf96c6a713d1c9bc9)
    • NSIS: src-tauri/target/release/bundle/nsis/Protected Registry_0.1.0_x64-setup.exe (sha256=1e0784168b13229b5f3709bd95fa9c0574d38f294156a872d22a0bdc354905b2)
  • pnpm --dir apps/protected-registry-desktop verify:native-artifacts confirms both installer artifacts and writes the local native-artifacts.json report.
  • The built release executable starts and remains running for a 5-second local smoke check, then closes cleanly.

CI evidence attachment checklist:

  • GitHub Actions workflow: .github/workflows/protected-registry-desktop.yml
  • Required job: Native package verify on windows-latest
  • Required green steps:
    • Verify native readiness
    • Verify production hardening packages
    • Cargo check Tauri host
    • Cargo test native command host
    • Build Windows desktop package
    • Smoke test built executable
    • Verify native package artifacts
    • actions/upload-artifact@v4
  • Required uploaded artifact: protected-registry-desktop-windows
  • Required artifact contents:
    • apps/protected-registry-desktop/native-artifacts.json
    • MSI installer under apps/protected-registry-desktop/src-tauri/target/release/bundle/**/*.msi
    • NSIS installer under apps/protected-registry-desktop/src-tauri/target/release/bundle/**/*.exe
  • Attach the Actions run URL and the native-artifacts.json SHA-256 entries to the branch or PR once CI completes.

The CLI requested here is a desktop-app CLI surface, not a CLI-only product.

Recommended shape:

Protected Registry Desktop App
-> graphical session and project/package view
-> embedded command runner
-> local credential/session vault
-> npm/pnpm/yarn wrapper
-> protected build and delivery commands

The command surface can still expose commands, but it belongs to the desktop application:

protected-registry login
protected-registry install @scope/package
protected-registry publish
protected-registry protect-build
protected-registry deliver --customer <customer_slug>

The desktop app owns the login session, token cache, prompts, project selection, customer selection, and local audit view. The command surface is a controlled automation layer inside that app.

developer
-> desktop app opens OIDC login
-> user selects project/package/action
-> app requests npm token from FractalOps
-> FractalOps checks SCIM entitlement
-> FractalOps returns short-lived .npmrc
-> app writes temp .npmrc
-> app runs npm/pnpm/yarn install with --userconfig
-> app deletes temp .npmrc
-> app records local and remote audit event

No long-lived npm token should be stored. The desktop app may cache the OIDC session or refresh token only through an OS-backed secure store.

browser
-> Nexus public URL
-> Pomerium OIDC
-> Keycloak identity and SCIM-projected groups
-> nexus-pomerium plugin
-> allow package read/publish by groups and package scope

The browser path is for registry UI and human inspection. Automated install and publish should prefer the desktop app broker path.

The package hardening pipeline must not be treated as simple minification.

source package
-> compile
-> tree-shake
-> minify
-> obfuscate
-> remove public source maps
-> create private debug/source-map artifact
-> create SBOM and provenance
-> create integrity manifest
-> publish protected package to Nexus
-> record evidence in FractalOps

uglified means code is harder to inspect. It does not mean encrypted. Customer delivery requires a separate encryption step.

protected package artifact
-> encrypt delivery bundle
-> store bundle in project delivery area
-> store key reference in OpenBao or KMS
-> record manifest and evidence
-> grant customer-specific access

Never store the decryption key beside the encrypted artifact.

Delivery artifact metadata:

{
"tenant_id": "default",
"project_slug": "example-project",
"customer_slug": "acme",
"package_name": "@customer/example-core",
"version": "1.0.0",
"protection": {
"obfuscated": true,
"encrypted": true,
"source_maps": "withheld"
},
"integrity": {
"sha256": "<artifact-sha256>"
},
"key_ref": "openbao://projects/example-project/customers/acme/package-delivery"
}

Recommended desktop app modules:

apps/protected-registry-desktop/
src/auth/
oidc-session
secure-store
src/fractalops/
portal-api-client
entitlement-client
credential-broker-client
src/package-manager/
npm-runner
pnpm-runner
yarn-runner
temp-npmrc
src/protection/
build-pipeline
obfuscation-policy
encryption-client
manifest-writer
src/delivery/
customer-bundle
project-artifact-store
evidence-writer
src/ui/
project-picker
package-picker
token-grant-view
delivery-view
src/cli/
command-router

Backend additions:

POST /v1/packages/registry/tokens/npm
POST /v1/packages/protection/builds
POST /v1/packages/delivery/bundles
GET /v1/packages/delivery/bundles/{bundle_id}
GET /v1/packages/delivery/view
POST /v1/packages/delivery/grants

These can reuse the existing PortalCredentialBrokerService and RegistryTokenService, but they need stricter package/project/action claims.

MVP should close one install loop first:

  1. Desktop app OIDC login.
  2. Project and package selection.
  3. SCIM entitlement check before token mint.
  4. Short-lived .npmrc returned from FractalOps.
  5. Desktop app runs package manager with temp .npmrc.
  6. Temp credential cleanup.
  7. Audit event recorded.

Second slice:

  1. Add publish token with package/action claims.
  2. Hardened build pipeline.
  3. Protected package publish.
  4. Customer encrypted delivery bundle.
  5. Portal evidence and delivery view.

Both MVP slices now have backend, desktop, and native-host contract coverage in this branch. Local Windows native verification has produced MSI and NSIS installers; CI packaging evidence remains pending until the Windows job runs.

These defaults are now encoded in the branch implementation and contract tests.

  1. Desktop stack: Tauri or Electron? Decision: Tauri.
  2. Package managers: npm only or npm/pnpm/yarn? Decision: support npm, pnpm, and yarn through the same short-lived token and per-command npmrc flow.
  3. Token lifetime: how short? Decision: 15 minutes for install, 5 minutes for publish.
  4. Does customer delivery include runtime decryption? Decision: no for MVP. Deliver encrypted bundle and key grant separately.
  5. Are source maps escrowed? Decision: yes. Keep private debug artifacts out of Nexus tarballs.
  6. Is publish allowed from developer machine? Decision: only for maintainers; CI remains normal publish path.
  7. Does Nexus enforce package-level claims? Decision: yes. FractalOps token mint and Nexus plugin both check.