Sign In

Documentation

Local setup

Get a registry running and point your toolchains at it.

Source: api.packr.blueforge.studio/-/docs/quickstart

Local Setup & Token Management

Overview

The Packr registry uses a single shared service account (blueforge-ci) for all internal BlueForge projects. This avoids proliferating user accounts and tokens β€” one token handles both publishing and installing @blueforge-studio packages across all repos.

Prerequisites

  • Node.js 22+ with pnpm
  • gh CLI authenticated (gh auth login)
  • Access to the Packr registry at https://api.packr.blueforge.studio

How It Works

ContextAuth MethodWhere Token Lives
Local development~/.npmrcToken stored directly in file
CI/CD (Gitea Actions)PACKR_TOKEN org secretOne org-level secret, every repo
blueforge-org monorepo.npmrc in repo rootReferences ${PACKR_TOKEN} env var

All contexts use the same blueforge-ci JWT token. The token is a standard npm auth token β€” pnpm, npm, and yarn all read it from .npmrc.

Local Development Setup

1. Global ~/.npmrc

Your ~/.npmrc should contain these lines (alongside any existing config):

# BlueForge Packr Registry
@blueforge-studio:registry=https://api.packr.blueforge.studio
//api.packr.blueforge.studio/:_authToken=<your-token>

This routes all @blueforge-studio/* package installs and publishes to Packr. All other packages continue to resolve from npmjs.org.

2. Per-Repo .npmrc (for CI)

Each repo that publishes or consumes @blueforge-studio packages should have a .npmrc in its root:

@blueforge-studio:registry=https://api.packr.blueforge.studio
//api.packr.blueforge.studio/:_authToken=${PACKR_TOKEN}

The ${PACKR_TOKEN} variable is resolved from the environment β€” set as a GitHub secret for CI, or from your shell for local use.

3. Store CI Credentials

The blueforge-ci password is stored locally for token rotation:

echo 'your-password-here' > ~/.packr-credentials
chmod 600 ~/.packr-credentials

This file is only read by the rotation script. It is never committed to any repo.

Publishing a Package

From any repo with a @blueforge-studio scoped package:

# Ensure package.json has publishConfig
# "publishConfig": { "registry": "https://api.packr.blueforge.studio" }

pnpm publish --access restricted

The ~/.npmrc token handles authentication automatically.

Publishing from CI (Gitea Actions)

The blueforge-org monorepo uses tag-based publishing. To publish from other repos, add a workflow like:

- uses: actions/setup-node@v4
  with:
    node-version: 22
    registry-url: https://api.packr.blueforge.studio
    scope: '@blueforge-studio'

- run: pnpm publish --access restricted
  env:
    NODE_AUTH_TOKEN: ${{ secrets.PACKR_TOKEN }}

Installing Packages

From any project with the .npmrc config above:

pnpm add @blueforge-studio/core-agent

No additional setup needed β€” the registry routing in .npmrc handles it.

Token Rotation

The rotation script mints a login JWT via POST /-/v1/login, so its lifetime is the login session's exp β€” about 30 days. The rotation script does not mint a registry token row, so none of the registry's own token semantics (scopes, roles, revocation via packr-cli token revoke) apply to it. exp is enforced on both auth paths; there is simply no expires_at column, so no API reports the deadline.

Running Rotation

cd /path/to/packr-registry
./scripts/rotate-token.sh

What It Updates

TargetHow
~/.npmrcReplaces the _authToken value in-place
GitHub repo secretsUpdates PACKR_TOKEN via gh secret set

Schedule

ItemValue
Credential typelogin JWT (not a registry token row)
Lifetime~30 days, from the JWT exp
Recommended rotationEvery 25 days (5-day buffer)
Script locationscripts/rotate-token.sh
Credentials file~/.packr-credentials (mode 600)

token_expiry_days and rotate_before_days are present in .packr.json but the script never reads them (scripts/rotate-token.sh only pulls registry, ci_user, scope, github_org, github_secret_visibility). Editing them changes nothing; the 30/25 numbers above are a convention, not a setting.

Central Configuration: .packr.json

All registry configuration lives in .packr.json at the repo root β€” the single source of truth for token rotation, repo list, and registry settings:

```json
{
  "registry": "https://api.packr.blueforge.studio",
  "scope": "@blueforge-studio",
  "ci_user": "blueforge-ci",
  "token_expiry_days": 30,
  "rotate_before_days": 25,
  "github_org": "blueforge-studio",
  "github_secret_visibility": "all"
}

There is no repos array, and the script does not iterate one. It sets a single org-level secret (gh secret set PACKR_TOKEN --org "$github_org", scripts/rotate-token.sh:114) that covers every repo in the org β€” which is why no per-repo list is needed.

FieldDescription
registryPackr registry URL
scopenpm scope routed to this registry
ci_userShared service account username
token_expiry_daysJWT token lifetime (set on server)
rotate_before_daysWhen to rotate (buffer before expiry)
reposGitHub repos that receive PACKR_TOKEN secret on rotation

Adding Repos to Rotation

Nothing to do. Rotation targets the whole org via github_org; a new repo under blueforge-studio inherits the secret automatically. An earlier revision of this document told you to add the repo to a repos array in .packr.json β€” that key does not exist in the file and the script never reads it, so following those instructions would have rotated nothing per-repo.

Manual Rotation

If the script is unavailable, rotate manually:

# 1. Get new token
curl -s -X POST https://api.packr.blueforge.studio/-/v1/login \
  -H 'Content-Type: application/json' \
  -d '{"name":"blueforge-ci","password":"<password>"}' | jq -r .token

# 2. Update ~/.npmrc with new token

# 3. Update each GitHub repo
gh secret set PACKR_TOKEN --body "<new-token>" --repo blueforge-studio/<repo>

Troubleshooting

ProblemCauseFix
401 Unauthorized on publishToken expiredRun ./scripts/rotate-token.sh
401 Unauthorized on installMissing .npmrc configAdd @blueforge-studio:registry line to ~/.npmrc
403 Forbidden on publishNot a package ownerFirst publish creates ownership; subsequent publishes require it
registration is disabledALLOW_REGISTRATION=falseExpected β€” use existing blueforge-ci account
tarball exceeds maximum sizePackage > 5MBSet MAX_TARBALL_SIZE env var on registry, or reduce package size

Architecture

Developer Machine                    Gitea Actions (forge-git)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ~/.npmrc     β”‚                     β”‚ PACKR_TOKEN      β”‚
β”‚ (auth token) β”‚                     β”‚ (org secret)     β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                                      β”‚
       β–Ό                                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       Cloudflare β†’ Caddy β†’ https://api.packr...     β”‚
β”‚              (Hetzner app-3, loopback :4873)         β”‚
β”‚                                                      β”‚
β”‚  Auth: JWT Bearer token (blueforge-ci)              β”‚
β”‚  Rate: 30 publish/min, 10 login/min per IP          β”‚
β”‚  Storage: Postgres (app_packr_registry) + S3        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Fly.io and Vercel are both decommissioned. Fly was destroyed 2026-09-09 (packr-registry.fly.dev no longer answers); the site moved off Vercel and deploys via forge-control. An earlier revision of this document described a packr-site Vercel project and gave a Vercel API recipe for reassigning packr.blueforge.studio β€” that recipe targets a platform this repo no longer uses, and the DNS record it describes is gone.

Deploys

ComponentHostHow
APIHetzner app-3, loopback :4873scripts/deploy-fleet.sh, or forge app-deploy --server --no-dns from a working dir
Dashboard siteHetzner app-2, port 3555forge-control, declared in apps/site/blueforge.config.json

The Caddy route for packr.blueforge.studio is written by the forge-control deployer; DNS points at the right host already, which is why the deploy path passes --no-dns.

One thing to know about the rate limits above: the per-IP keys are derived from Fly-Client-IP first and then the rightmost X-Forwarded-For entry (internal/ratelimit/ratelimit.go:191). Fly is gone, so the first header is client-settable and the second is the closest proxy's value rather than the client's. The limits are not currently enforced against a trustworthy client IP β€” see the defect note in SECURITY.md.