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 (GitHub Actions) | PACKR_TOKEN secret | GitHub repo secret per 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 public
The ~/.npmrc token handles authentication automatically.
Publishing from CI (GitHub 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 public
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
Tokens expire every 30 days. The rotation script regenerates the token and updates all configured locations.
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 |
|---|---|
| Token lifetime | 30 days |
| Recommended rotation | Every 25 days (5-day buffer) |
| Script location | scripts/rotate-token.sh |
| Credentials file | ~/.packr-credentials (mode 600) |
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:
{
"registry": "https://api.packr.blueforge.studio",
"scope": "@blueforge-studio",
"ci_user": "blueforge-ci",
"token_expiry_days": 30,
"rotate_before_days": 25,
"repos": [
"blueforge-studio/blueforge-org",
"blueforge-studio/restaurant-os",
"blueforge-studio/investor-os"
]
}
| 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
Add the repo to the repos array in .packr.json:
{
"repos": [
"blueforge-studio/blueforge-org",
"blueforge-studio/restaurant-os",
"blueforge-studio/new-project"
]
}
Then run ./scripts/rotate-token.sh — it reads from .packr.json automatically.
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 GitHub Actions
┌─────────────┐ ┌──────────────────┐
│ ~/.npmrc │ │ PACKR_TOKEN │
│ (auth token) │ │ (GitHub secret) │
└──────┬──────┘ └────────┬─────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────┐
│ https://api.packr.blueforge.studio │
│ (Fly.io — ams) │
│ │
│ Auth: JWT Bearer token (blueforge-ci) │
│ Rate: 30 publish/min, 10 login/min per IP │
│ Limits: 5MB tarball, 20 tokens/user │
└─────────────────────────────────────────────────────┘
DNS Configuration
The packr dashboard site (packr-site Vercel project) serves at packr.blueforge.studio.
Important: The packr.blueforge.studio subdomain must be assigned to the packr-site Vercel project (not the main site or forge-platform project). If packr-cli login shows a 404 on /cli/auth, the domain is assigned to the wrong project.
Fix via Vercel API:
# Remove from wrong project
curl -X DELETE "https://api.vercel.com/v9/projects/{wrong-project-id}/domains/packr.blueforge.studio?teamId={team-id}" \
-H "Authorization: Bearer $VERCEL_TOKEN"
# Add to packr-site project
curl -X POST "https://api.vercel.com/v10/projects/{packr-site-project-id}/domains?teamId={team-id}" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "packr.blueforge.studio"}'
Cloudflare DNS: packr.blueforge.studio → CNAME → cname.vercel-dns.com (already configured in zone b2a7319881e0600969600b0572793515).