Skip to content

Private package releases (Keygen registry)

LumenFlow 1.0 packages are distributed from an owner-operated, licence-gated Keygen registry. pnpm release publishes from the owner’s machine through the Keygen REST API: every tarball is verified byte-for-byte before upload, uploaded to private storage, read back and compared, and only then published. There is no npm publish, no hosted CI and no GitHub Packages; GitHub holds source only.

PurposeURL
Consumer npm registryhttps://licensing.lumenflow.dev/v1/engines/npm/
Keygen REST API (publisher)https://licensing.lumenflow.dev/v1
Private signed downloadshttps://downloads.lumenflow.dev

Every publishable @hellmai/lumenflow-* manifest declares publishConfig.registry = the consumer registry and publishConfig.access = restricted. Two manifests switch their registry declaration in their own work units rather than here: @hellmai/lumenflow-cli (WU-4062, consumer transport) and the surfaces package (WU-4058, source-leakage guard). Until both land, those two manifests still name the previous registry; the publisher never reads publishConfig.registry to choose a destination, so the mismatch cannot redirect an upload.

Operator prerequisites (rc.1 rehearsal, WU-4066)

Section titled “Operator prerequisites (rc.1 rehearsal, WU-4066)”

Before running pnpm release, confirm:

  • A current consumer npmrc. Set NPM_CONFIG_USERCONFIG (or pass --consumer-npmrc) to a licence npmrc minted from a current public-host free or paid grant. Older local-edge grants fail closed with 401; mint a fresh grant rather than reusing a stale one — the release’s registry lineage probe reads through this credential.
  • The primary main checkout, not a worktree. The release CLI’s ensureOnMain guard refuses any other branch or checkout by design.
  • A clean, freshly built dist. Run turbo clean then build:dist before relying on dist-derived output, or let the CLI’s own artifact-coherence check trigger the rebuild automatically. Stale compiled output blocks the run.

See the rc.1 release rehearsal verification report (WU-4066) for the full round-by-round history these prerequisites were discovered from.

  1. Point the release at the credential file (paths only; never export a token):

    export LUMENFLOW_KEYGEN_CREDENTIAL_FILE=/home/tom/services/lumenflow-licensing/secrets/publisher.json

    The file must be an owner-only regular file (mode 0600) whose apiUrl, productId and storageOrigin match the deployment. To point at another deployment, name a JSON config file with LUMENFLOW_KEYGEN_CONFIG_FILE (keys: apiUrl, storageOrigin, productId, credentialFile, journalDir, receiptDir). LUMENFLOW_KEYGEN_JOURNAL_DIR and LUMENFLOW_KEYGEN_RECEIPT_DIR override the two directories.

  2. Rehearse without uploading anything:

    pnpm release --release-version 1.0.0-rc.1 --dry-run

    The full validation set runs in a disposable worktree. No Keygen upload, no pack-registry upload, and no credential is read. The version is checked against the Keygen channel model before anything else runs, so a version the registry would refuse (for example 1.0.0-preview.1) stops here with nothing modified.

  3. Publish:

    pnpm release --release-version 1.0.0-rc.1

    1.0.0-rc.1 goes to the rc channel; 1.0.0 goes to stable. The stable channel never receives a prerelease. --skip-publish bumps, validates and tags only: zero package uploads and zero pack-registry uploads, and no credential is read.

  4. Read the summary. For each package it prints an exact-archive receipt path (default ~/.lumenflow/keygen-receipts/publish-<artifactId>.json) holding the package, version, channel, Keygen ids, sha256, sha512 and storedBytesVerified: true.

The minify-and-mangle build stage (fail-closed)

Section titled “The minify-and-mangle build stage (fail-closed)”

After every build:dist (on the dry-run and real paths alike, and even with --skip-build) the release minifies every implementation .js/.mjs file under each publishable package’s dist output and the CLI’s bundled packs, before pack-integrity refresh, validation, consumer-install checks and packing. There is no flag to skip it. The stage runs esbuild in process, per file, with no bundling: format: 'esm', platform: 'node', target: 'node26', whitespace, syntax and identifier minification (keepNames: true so the fn.name audit field two authorization paths read on survives), charset: 'utf8', the shebang preserved verbatim, no source map, legalComments: 'none'. esbuild is resolved from the workspace pnpm store through vitest’s own dependency, so this stage adds no new root dependency. An unresolvable esbuild or a per-file transform error aborts the release, naming the file.

This replaces an earlier WU-4067 protected/bytecode build candidate that ADR-122, Private Distribution Posture (docs/09-architecture-decisions/ADR-122-private-distribution-posture.md) rejected after its independent review found a confirmed defect on real product modules and public decompilers for that artifact class. See that ADR for the full decision record: the licence gate is the access control, not the artifact; minification is readability friction, not a security boundary, and is not represented as one anywhere in this guide.

ModeKeygen package uploadPack-registry uploadCredential readTag pushed
--dry-runnonenonenono
--skip-publishnonenone (read-only plan)noyes
defaultverified archiveslagging packs onlyowner-only fileyes

Pushing the v* tag to GitHub cannot publish anything: the repository holds source only, GitHub Actions is disabled for verification and publication, and the former publish-on-tag workflow was removed by WU-4060. The only publication route is the local pnpm release command on the owner’s machine.

  • The uploaded bytes are the packed bytes that were verified: no non-declaration TypeScript, no .map files, no inline data: source maps, no __tests__ entries, no links or unsafe paths, a complete gzip stream and tar end marker.
  • Uploads go only to a signed URL on the configured https storage host; redirects are never followed, and the API token is sent only to the API origin.
  • The stored object is downloaded and byte-compared before the release is published. Keygen’s UPLOADED status alone is not trusted.
  • Re-running a release with identical bytes verifies the stored archive and creates no duplicate. Different bytes at an existing version are refused.
  • An interrupted upload resumes the same draft from a protected local journal of signed upload links. Failed processing is reported for inspection, never deleted.

Access is granted per recipient with the licensing service on the owner server:

python3 scripts/manage-local.py grant --tier free --name 'Customer name'
python3 scripts/manage-local.py grant --tier paid --name 'Customer name' --days 365
python3 scripts/manage-local.py suspend LICENSE_UUID

Each grant writes a consumer.npmrc into its own protected directory. Share only that recipient’s file, over your own secure channel. “Paid” is an owner-assigned policy label: no signup, payment or billing automation exists. Suspension blocks new downloads; it cannot remove copies that were already installed or cached.

Put the granted consumer.npmrc outside the repository and name it when installing:

npm install @hellmai/lumenflow-cli@1.0.0-rc.1 --userconfig /secure/consumer.npmrc

or export NPM_CONFIG_USERCONFIG=/secure/consumer.npmrc for the shell. The release tooling’s own consumer checks (the packed-consumer smoke and the upgrade matrix) use the same variable, or --consumer-npmrc <path>; they never read a GitHub token. 401 or 403 means the credential is missing or wrong. A 404 for the product package is accepted as “first release” only after the tooling has read the packument of the published licensing fixture @hellmai/licensing-smoke-test through the same registry URL and credential; if that fixture is unreachable the run fails as a registry misconfiguration instead of silently skipping the upgrade matrix.

The release rehearsal publishes the workspace to an owned loopback registry through the registry HTTP API after a real pnpm pack. It never spawns pnpm publish, whose destination the manifest’s publishConfig.registry controls, so the production registry cannot receive rehearsal traffic, and a non-loopback staging registry is refused outright. “Loopback” is decided by parsing the host as an IP address (127.0.0.0/8, ::1, localhost), so a registrable name such as 127.attacker.example is refused. Prereleases are staged under their channel dist-tag (rc, beta, …); only a stable version is tagged latest, as on a real registry.

MessageMeaning and fix
Registry authentication not foundNo credential file configured; set LUMENFLOW_KEYGEN_CREDENTIAL_FILE or use --skip-publish.
must be an owner-only regular filechmod 0600 the credential file and make sure your user owns it.
do not match the configured deploymentThe file’s apiUrl/productId/storageOrigin differ from the resolved config; fix one side.
Existing version has different archive bytesThe version is already published with other bytes; cut a new version, never replace.
Unexpected private storage destination / redirect refusedKeygen offered a link off the configured storage host or the host redirected; inspect the deployment.
Artifact processing failed; inspect the draft before retryLook at the draft release in the portal; the publisher will not delete or replace it.
Package processing timed out; retry resumes the same draftRe-run; the journal resumes the same upload link while it is valid.
the minify stage could not resolve esbuild from the workspace pnpm storeRun pnpm install and retry; esbuild ships as a vitest dependency, not a new root dependency.
the minify stage failed to transform <file>A per-file esbuild transform error; the release fails closed and names the file.
cannot be published to the private Keygen registryThe version label is not rc/beta/alpha/dev.N or stable semver; nothing was modified.
Registry misconfiguration: ... answered 404 for ... ANDThe consumer-check registry URL or scope is wrong; the licensing fixture must be readable.
  • Licence grants, renewals and revocations are manual owner actions.
  • Distribution control is not code protection: a licensed recipient can read the minified JavaScript they install. ADR-122 accepts that boundary explicitly rather than claiming a stronger guarantee; obfuscation and compiled/bytecode routes were evaluated and rejected.
  • The rc.1 licensed consumer proof (fresh free and paid installs, anonymous and revoked denial) is a separate verification against the live service; the publisher’s own tests use owned loopback fixtures.
  • On Windows the credential-file mode check is skipped; keep the file private by other means.