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
ghCLI authenticated (gh auth login)- Access to the Packr registry at
https://api.packr.blueforge.studio
How It Works
| Context | Auth Method | Where Token Lives |
|---|---|---|
| Local development | ~/.npmrc | Token stored directly in file |
| CI/CD (Gitea Actions) | PACKR_TOKEN org secret | One org-level secret, every repo |
| blueforge-org monorepo | .npmrc in repo root | References ${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
| Target | How |
|---|---|
~/.npmrc | Replaces the _authToken value in-place |
| GitHub repo secrets | Updates PACKR_TOKEN via gh secret set |
Schedule
| Item | Value |
|---|---|
| Credential type | login JWT (not a registry token row) |
| Lifetime | ~30 days, from the JWT exp |
| Recommended rotation | Every 25 days (5-day buffer) |
| Script location | scripts/rotate-token.sh |
| Credentials file | ~/.packr-credentials (mode 600) |
token_expiry_daysandrotate_before_daysare present in.packr.jsonbut the script never reads them (scripts/rotate-token.shonly pullsregistry,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.
| Field | Description |
|---|---|
registry | Packr registry URL |
scope | npm scope routed to this registry |
ci_user | Shared service account username |
token_expiry_days | JWT token lifetime (set on server) |
rotate_before_days | When to rotate (buffer before expiry) |
repos | GitHub 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
| Problem | Cause | Fix |
|---|---|---|
401 Unauthorized on publish | Token expired | Run ./scripts/rotate-token.sh |
401 Unauthorized on install | Missing .npmrc config | Add @blueforge-studio:registry line to ~/.npmrc |
403 Forbidden on publish | Not a package owner | First publish creates ownership; subsequent publishes require it |
registration is disabled | ALLOW_REGISTRATION=false | Expected β use existing blueforge-ci account |
tarball exceeds maximum size | Package > 5MB | Set 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.devno longer answers); the site moved off Vercel and deploys via forge-control. An earlier revision of this document described apackr-siteVercel project and gave a Vercel API recipe for reassigningpackr.blueforge.studioβ that recipe targets a platform this repo no longer uses, and the DNS record it describes is gone.
Deploys
| Component | Host | How |
|---|---|---|
| API | Hetzner app-3, loopback :4873 | scripts/deploy-fleet.sh, or forge app-deploy --server --no-dns from a working dir |
| Dashboard site | Hetzner app-2, port 3555 | forge-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-IPfirst and then the rightmostX-Forwarded-Forentry (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 inSECURITY.md.