Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
env:
REVA_TURBO_DIR: ${{ github.workspace }}
REVA_TURBO_SKIP_GIT: "1"
run: bash install.sh
run: bash plugin/install.sh

- name: Verify install artifacts
shell: bash
Expand All @@ -45,7 +45,7 @@ jobs:
env:
REVA_TURBO_DIR: ${{ github.workspace }}
REVA_TURBO_SKIP_GIT: "1"
run: bash install.sh
run: bash plugin/install.sh

lint-shell:
name: shellcheck (install.sh + bin/)
Expand All @@ -56,8 +56,8 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run shellcheck
run: |
shellcheck install.sh setup bin/reva-turbo-* \
skills/*/bin/*.sh 2>&1 | tee shellcheck.log || true
shellcheck plugin/install.sh plugin/setup plugin/bin/reva-turbo-* \
plugin/skills/*/bin/*.sh 2>&1 | tee shellcheck.log || true
# Fail on SC2000-series errors only (real bugs, not style):
if grep -E "error:" shellcheck.log; then
echo "shellcheck found errors"
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,20 @@ RevOps-RevAMfg/

## For end users (Rev A PMs)

Your admin deploys the backend once. Then you run one command on your machine:
Your admin deploys the backend once and shares the router URL + a one-time signup token. Then you run:

```bash
curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/install.sh \
| REVA_MCP_URL=https://<router>.up.railway.app/mcp \
REVA_API_KEY=nk_... \
bash
| REVA_MCP_URL=https://<router>.up.railway.app/mcp bash
```

Restart Claude Code, then `/reva-turbo:revmyengine`. The engine is now connected to the shared CRM and memory — everything you log is available to the whole team.
The installer drops into a short wizard that prompts for your name, email, a password (12+ chars — you'll only need it to reset your key), and the signup token. Under the hood it calls the router's `/signup` endpoint, which mints a personal `nk_...` API key scoped to your user and writes it into `~/.claude/mcp.json`.

Prefer the browser? Visit `https://<router>.up.railway.app/signup` instead and you'll get the same key + an exact install command to paste.

Restart Claude Code, then `/reva-turbo:revmyengine`. The engine is now connected to the shared CRM and memory — everything you log is available to the whole team, and every action is attributed to your user on the Nakatomi timeline.

See [`docs/AUTH.md`](./docs/AUTH.md) for the full auth flow and rotation story.

## For admins (MrDula Solutions)

Expand Down
168 changes: 168 additions & 0 deletions docs/AUTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Authentication and Signup

End-to-end: how a Rev A PM goes from "never heard of REVA-OPS" to a
working Claude Code connection in about 90 seconds.

## Two principals, two paths

| Principal | Credentials | How they get them |
|-----------------|-----------------------------------|--------------------------------------------|
| Admin (MrDula) | Railway account + admin `nk_...` | `./railway/deploy.sh` prints both |
| PM (Rev A user) | Personal `nk_...` API key | Self-serve via `/signup` page **or** `install.sh` wizard |

There is no shared-key-for-the-team path. Every PM has their own key so
Nakatomi's timeline attributes activities, notes, and deal moves to the
right person.

## Admin flow — one-time

```bash
./railway/deploy.sh --project-name reva-ops --admin-email admin@reva-mfg.com
```

`deploy.sh` does this (and prints all of it):

1. Creates the Railway project.
2. Provisions Postgres, FalkorDB, Qdrant.
3. Deploys `mcp-router`, `nakatomi-backend`, `automem-backend` from their source repos.
4. Waits for migrations to run inside Nakatomi on boot.
5. Runs `python -m scripts.seed` on `nakatomi-backend` → creates:
- The Rev A workspace (`slug: reva`)
- An admin user with `role: owner`
- An initial admin API key (`nk_...`)
6. Runs `services/nakatomi-backend/seed/reva.py` → installs the Rev A
pipeline + custom-field manifest.
7. Sets two shared env vars on the `mcp-router` service:
- `NAKATOMI_ADMIN_TOKEN` = the admin API key from step 5
- `REVA_SIGNUP_TOKEN` = a freshly generated shared signup gate
8. Prints the public MCP URL, admin email / password / API key, and the
signup token.

The admin keeps the admin key. The signup token is what they share with
PMs during onboarding (Slack DM, 1Password, whatever).

## PM flow — one-time per PM

### Option 1 — terminal wizard (recommended)

```bash
curl -fsSL https://raw.githubusercontent.com/mrdulasolutions/RevOps-RevAMfg/main/plugin/install.sh \
| REVA_MCP_URL=https://<router>.up.railway.app/mcp bash
```

Because `REVA_API_KEY` is *not* set, `install.sh` drops into the wizard:

```
Your name : Jane Doe
Work email : jane@reva-mfg.com
Password (12+ chars) : ************
Signup token : (paste what the admin sent)
```

Under the hood it POSTs to `<router>/signup`, captures the returned
`nk_...` key, and writes it into `~/.claude/mcp.json`. Restart Claude
Code. Done.

### Option 2 — browser

Visit `https://<router>.up.railway.app/signup`. Same inputs, same
result. The page displays the key once and gives you the exact
`install.sh` command to run with `REVA_API_KEY` pre-filled.

### Option 3 — pre-shared key

If an admin already has a key they want to hand you:

```bash
curl -fsSL https://.../plugin/install.sh \
| REVA_MCP_URL=... REVA_API_KEY=nk_... bash
```

## What `/signup` actually does

Nakatomi's native `POST /auth/signup` only creates fresh workspaces —
there's no public "join an existing workspace" endpoint. The router
stitches the flow together:

```
PM → POST /signup {name, email, password, signup_token}
│ router: validate signup_token (constant-time compare)
POST nakatomi/auth/signup ← public; creates user +
throwaway personal workspace
│ returns user_id
POST nakatomi/workspace/members ← admin token; adds user to Rev A
(headers: Authorization: Bearer <NAKATOMI_ADMIN_TOKEN>,
X-Workspace: reva)
POST nakatomi/workspace/api-keys ← admin token; mints key for user
│ returns plaintext key (only time it's ever returned)
PM ← {api_key, user_id, workspace_slug, mcp_url}
```

The throwaway personal workspace is never referenced — it's a
side-effect of Nakatomi's signup contract. Harmless.

## Rotation

- **Signup token** — redeploy `mcp-router` with a new `REVA_SIGNUP_TOKEN`
env. Old token stops working instantly. Existing PMs are unaffected
(their keys are minted; they don't need the signup token anymore).
- **PM API key** — Nakatomi's `/workspace/api-keys/{id}` DELETE endpoint
revokes. PM re-runs the signup wizard.
- **Admin token** — admin mints a replacement via Nakatomi's own API or
through the router's admin calls (not yet exposed), then updates
`NAKATOMI_ADMIN_TOKEN` on the router.

## What `install.sh` writes

After a successful signup:

```
~/.claude/mcp.json ← adds "reva" MCP server entry
~/.reva-turbo/config.yaml ← reva_mcp_url + reva_api_key
~/.claude/skills/reva-turbo ← symlink to the cloned plugin dir
```

`~/.claude/mcp.json` after install:

```json
{
"mcpServers": {
"reva": {
"type": "http",
"url": "https://<router>.up.railway.app/mcp",
"headers": { "Authorization": "Bearer nk_xxx..." }
}
}
}
```

Claude Code picks that up on next restart.

## Security notes

- `/signup` requires `REVA_SIGNUP_TOKEN` AND `NAKATOMI_ADMIN_TOKEN` to
be set on the router. Missing either disables the endpoint (returns
503). A pristine deploy with no admin interaction is closed by default.
- Passwords are sent over TLS to the router, forwarded over the Railway
private network to Nakatomi, and hashed with bcrypt before storage.
- The signup token is compared with `secrets.compare_digest` — no
timing leaks.
- API keys are stored hashed (SHA-256) in Nakatomi; plaintext is only
returned at mint time. Losing the key means minting a new one.
- The admin token never leaves the router (not forwarded to the PM,
not in logs, not in the MCP response). If a router container image is
compromised, rotate `NAKATOMI_ADMIN_TOKEN`.

## Known gaps (tracked for v2.1)

- No email verification. Signup token is the only gate. Fine for an
internal Rev A team; not OK for public deploys.
- No password reset flow in the router. If a PM forgets, admin has to
mint a new key via Nakatomi directly and hand it over.
- No SSO (SAML/OIDC). Tracked in `docs/ROADMAP.md`.
51 changes: 51 additions & 0 deletions plugin/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,57 @@ if [ -n "${REVA_API_KEY:-}" ] && [ -x "$CONFIG_CMD" ]; then
say "Saved reva_api_key to config.yaml"
fi

# ── Step 6a: interactive signup wizard ──────────────────────────────────
# If REVA_MCP_URL is set but REVA_API_KEY is not, offer to mint one. The
# router hosts /signup that takes {name, email, password, signup_token}
# and returns an API key. We POST directly so the whole flow happens in
# the terminal.
if [ -n "${REVA_MCP_URL:-}" ] && [ -z "${REVA_API_KEY:-}" ] && [ -t 0 ]; then
# Derive the signup URL from the MCP URL (strip trailing /mcp* suffix).
SIGNUP_URL="${REVA_MCP_URL%/mcp*}/signup"
say "No REVA_API_KEY provided — running signup wizard."
say " Signup endpoint: $SIGNUP_URL"

printf "Your name : "; read -r REVA_NAME
printf "Work email : "; read -r REVA_EMAIL
printf "Password (12+ chars) : "; stty -echo 2>/dev/null; read -r REVA_PASSWORD; stty echo 2>/dev/null; printf "\n"
printf "Signup token : "; read -r REVA_TOKEN

if [ -z "${REVA_NAME}" ] || [ -z "${REVA_EMAIL}" ] || [ -z "${REVA_PASSWORD}" ] || [ -z "${REVA_TOKEN}" ]; then
say "Signup skipped — one or more fields empty. Re-run with REVA_API_KEY=... to finish."
elif command -v python3 >/dev/null 2>&1; then
REVA_API_KEY="$(python3 - "$SIGNUP_URL" "$REVA_NAME" "$REVA_EMAIL" "$REVA_PASSWORD" "$REVA_TOKEN" <<'PY'
import json, sys, urllib.request, urllib.error
url, name, email, password, token = sys.argv[1:]
body = json.dumps({
"display_name": name, "email": email,
"password": password, "signup_token": token,
}).encode()
req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
print(data["api_key"])
except urllib.error.HTTPError as e:
body = e.read().decode(errors="replace")
sys.stderr.write(f"signup failed: {e.code} {body}\n")
sys.exit(1)
except Exception as e:
sys.stderr.write(f"signup failed: {e}\n")
sys.exit(1)
PY
)"
if [ -n "$REVA_API_KEY" ]; then
say "✓ API key minted and will be saved to ~/.claude/mcp.json"
[ -x "$CONFIG_CMD" ] && "$CONFIG_CMD" set reva_api_key "$REVA_API_KEY"
else
say "Signup failed — see error above. Retry later with REVA_API_KEY=... set."
fi
else
say "python3 not found — cannot run signup wizard. Visit $SIGNUP_URL in your browser instead."
fi
fi

# ── Step 7: register REVA MCP in Claude Code's mcp.json ─────────────────
# Only when both URL and key are set. We write JSON by hand (no jq
# dependency) using a tiny Python one-liner if Python is available, else
Expand Down
11 changes: 11 additions & 0 deletions railway/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ services:
AUTOMEM_API_TOKEN: ${{ shared.AUTOMEM_API_TOKEN }}
CRM_TOOL_PREFIX: crm
MEM_TOOL_PREFIX: mem
# Self-service signup (PM onboarding via /signup)
REVA_SIGNUP_TOKEN: ${{ shared.REVA_SIGNUP_TOKEN }}
NAKATOMI_ADMIN_TOKEN: ${{ shared.NAKATOMI_ADMIN_TOKEN }}
REVA_WORKSPACE_SLUG: reva
PUBLIC_MCP_URL: https://${{ RAILWAY_PUBLIC_DOMAIN }}/mcp

# 2. Nakatomi (CRM) — internal only.
- name: nakatomi-backend
Expand Down Expand Up @@ -100,3 +105,9 @@ sharedVariables:
- name: OPENAI_API_KEY
description: "Optional — enables real embeddings."
required: false
- name: REVA_SIGNUP_TOKEN
description: "Shared signup gate PMs need to mint a key. Rotate by redeploying."
generator: "hex:16"
- name: NAKATOMI_ADMIN_TOKEN
description: "Admin nk_... key for the Rev A workspace. Set by deploy.sh after the admin user is seeded."
required: false
18 changes: 18 additions & 0 deletions services/mcp-router/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,21 @@ MEM_TOOL_PREFIX=mem
# Timeouts (seconds)
UPSTREAM_TIMEOUT=30
UPSTREAM_CONNECT_TIMEOUT=5

# ── Self-service signup (PM onboarding) ────────────────────────────────────
# Both values must be set to enable /signup. Leave blank to disable.
#
# REVA_SIGNUP_TOKEN Shared signup gate — the admin shares this with PMs
# during onboarding. Rotate it by redeploying with a
# new value. Generate: openssl rand -hex 16
# NAKATOMI_ADMIN_TOKEN Admin nk_... API key (role=owner) for the Rev A
# workspace. The router uses this to add new users
# as members and mint their personal API keys.
# REVA_WORKSPACE_SLUG Slug of the Rev A workspace in Nakatomi (default: reva).
# PUBLIC_MCP_URL Public URL the signup page tells users to point
# clients at. Set automatically by Railway (usually
# https://<router-domain>/mcp).
REVA_SIGNUP_TOKEN=
NAKATOMI_ADMIN_TOKEN=
REVA_WORKSPACE_SLUG=reva
PUBLIC_MCP_URL=
5 changes: 5 additions & 0 deletions services/mcp-router/router/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from mcp.server.transport_security import TransportSecuritySettings

from .config import settings
from .signup import router as signup_router
from .tools import crm, cross, memory


Expand Down Expand Up @@ -42,6 +43,9 @@ def create_app() -> FastAPI:
mcp = build_mcp()
app.mount("/mcp", mcp.streamable_http_app())

# Self-service signup (GET /signup HTML, POST /signup JSON)
app.include_router(signup_router)

@app.get("/health")
async def health() -> JSONResponse:
return JSONResponse({"ok": True, "service": "reva-mcp-router"})
Expand All @@ -52,6 +56,7 @@ async def index() -> JSONResponse:
{
"service": "reva-mcp-router",
"mcp_endpoint": "/mcp/",
"signup_page": "/signup",
"tool_prefixes": {
"crm": settings.crm_tool_prefix,
"memory": settings.mem_tool_prefix,
Expand Down
Loading
Loading