# 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

| 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:

```bash
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:

```bash
# 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:

```yaml
- 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:

```bash
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

```bash
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_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
```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:

```bash
# 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.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

| 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-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`](SECURITY.md#rate-limiting).
