Skip to content
Open
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
2,662 changes: 2,115 additions & 547 deletions package-lock.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions packages/migrate-tool/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ----- Frontend (Vite — must be prefixed VITE_) -----

# Audius developer app API key (safe to expose in browser).
# Get one at audius.co/settings → Developer Apps.
VITE_AUDIUS_API_KEY=

# "production" (default) or "development" to point at a local protocol stack.
VITE_AUDIUS_ENVIRONMENT=production

# ----- Backend (Vercel functions — never expose in browser) -----

# Audius bearer token. Backend only. Used with the API key above so the
# server can act on behalf of users who have authorized the developer app.
AUDIUS_API_KEY=
AUDIUS_BEARER_TOKEN=

# Bearer secret that admin endpoints (list/approve/reject) require in the
# Authorization header. Pick a long random string and share it only with
# Audius team members authorized to approve migrations.
ADMIN_BEARER_TOKEN=

# Supabase connection. The service role key bypasses RLS and must stay
# backend-only — never expose it.
SUPABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
7 changes: 7 additions & 0 deletions packages/migrate-tool/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.vercel
.env
.env.local
*.log
.turbo
129 changes: 129 additions & 0 deletions packages/migrate-tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# @audius/migrate-tool

A small web tool for moving an artist's tracks from an old Audius account to a
new one — for cases where the artist has lost access to their original account
(forgotten password, lost email) and has created a new account.

Designed to be deployed to **migrate.audius.co** on Vercel with a Supabase
database backing the request queue.

## How it works

1. The artist signs in with their new Audius account (OAuth via the Audius
developer app).
2. They enter the handle of their old account. The tool previews the tracks
that would be migrated.
3. They submit a migration request. The request is stored in Supabase with
`status = 'pending'`.
4. An Audius team member opens `/admin`, unlocks with the admin bearer token,
and reviews the request. Identity verification (confirming the requester
actually owns the old account) happens **out-of-band** via the usual
support channel — the tool does not enforce it.
5. On approval the backend pulls each old track's audio + artwork via the SDK
and re-uploads it on the new account using the developer app's bearer
token. Per-track results are written back to the DB and shown on the
status page.

## Limitations

- **Audio source selection**: the worker tries sources in priority order
for each track and uses the first one that succeeds:
1. Raw `orig_file_cid` fetched from the rendezvous-primary validator node
(`/content/{cid}`) — bit-for-bit copy of the original master,
regardless of whether the artist marked the track downloadable.
2. Same CID, mirror node — covers the case where the primary node is
unhealthy.
3. Gated download URL — original master, only available when the track
is flagged downloadable.
4. Transcoded MP3 stream — lossy, but always reachable. Acts as the
last-resort fallback so every approved request gets a content copy.
Sources 1 and 2 are skipped when `orig_file_cid` is missing or
`is_original_available` is false (legacy uploads or pruned originals).
- **No identity verification in-tool**: anyone signed in can request migration
of any handle. The approver is responsible for verifying the requester owns
the old account before approving. Don't approve a request without
confirming identity through a separate channel.
- **Old account is not modified**: the tracks are re-created on the new
account. The originals on the old account are untouched (the tool has no
authority over the old account).
- **No social-graph preservation**: plays, favorites, reposts, and comments
on the old tracks do not carry over.

## Deploy

### 1. Supabase

Create a project, then run the SQL in `supabase/migrations/0001_init.sql`.

You'll need:

- `SUPABASE_URL` — project URL
- `SUPABASE_SERVICE_ROLE_KEY` — backend-only key (do not expose to the browser)

### 2. Audius developer app

Create a developer app at <https://audius.co/settings> → Developer Apps. You'll
get an **API Key** and a **Bearer Token**.

- `VITE_AUDIUS_API_KEY` — the API key (safe in the browser; baked into the build)
- `AUDIUS_API_KEY` — same API key, for the backend
- `AUDIUS_BEARER_TOKEN` — backend-only; grants the app permission to act on
behalf of users who have authorized it via OAuth

You'll also need to whitelist the deployment's OAuth redirect URI in the dev
app's settings (e.g. `https://migrate.audius.co/`).

### 3. Admin token

- `ADMIN_BEARER_TOKEN` — pick a long random string. Share it only with team
members authorized to approve migrations.

### 4. Vercel

```sh
cd packages/migrate-tool
npx vercel link
npx vercel env add VITE_AUDIUS_API_KEY
npx vercel env add AUDIUS_API_KEY
npx vercel env add AUDIUS_BEARER_TOKEN
npx vercel env add ADMIN_BEARER_TOKEN
npx vercel env add SUPABASE_URL
npx vercel env add SUPABASE_SERVICE_ROLE_KEY
npx vercel --prod
```

Then add `migrate.audius.co` as a domain in the Vercel project.

## Local development

```sh
cp .env.example .env.local
# Fill in the values, then:
npm install
npm run dev
```

The Vite dev server runs at `http://localhost:5180`. To exercise the API
functions locally, run them with `npx vercel dev` instead.

## Approving a request

1. Go to `https://migrate.audius.co/admin`.
2. Paste the `ADMIN_BEARER_TOKEN` to unlock.
3. Review the request — especially the old handle and the track list.
4. **Verify the requester owns the old account through your usual support
channel.** This is the only safeguard against migration abuse.
5. Click **Approve & execute**. The backend runs the migration synchronously
(Vercel function timeout is set to 5 minutes in `vercel.json`).

## Files

- `src/` — Vite + React SPA (home / status / admin pages)
- `api/` — Vercel serverless functions
- `api/requests/index.ts` — `POST /api/requests` (create)
- `api/requests/[id].ts` — `GET /api/requests/:id` (status)
- `api/admin/requests.ts` — `GET /api/admin/requests` (list, bearer-gated)
- `api/admin/approve.ts` — `POST /api/admin/approve?id=…` (bearer-gated)
- `api/admin/reject.ts` — `POST /api/admin/reject?id=…` (bearer-gated)
- `api/_lib/migrate.ts` — migration worker
- `supabase/migrations/0001_init.sql` — DB schema
37 changes: 37 additions & 0 deletions packages/migrate-tool/api/_lib/audius.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
createSdkWithServices,
type AudiusSdkWithServices
} from '@audius/sdk'

let serverSdk: AudiusSdkWithServices | null = null

/**
* Server-side SDK initialized with the developer app's API key + bearer
* token. The bearer token grants the app permission to act on behalf of
* any user who has authorized it via OAuth.
*
* Per the SDK README: "Bearer Token — backend only. Grants your app the
* ability to act on behalf of users who have authorized it. Never expose
* this in browser or mobile code."
*
* We use createSdkWithServices (rather than the public sdk() factory) so
* sdk.tracks is the wrapped TracksApi with friendly helpers like
* getTrackStreamUrl, getTrackDownloadUrl, and the createTrack overload
* that handles audio + image upload + trackCid in one call.
*/
export function getServerSDK(): AudiusSdkWithServices {
if (serverSdk) return serverSdk
const apiKey = process.env.AUDIUS_API_KEY
const bearerToken = process.env.AUDIUS_BEARER_TOKEN
if (!apiKey || !bearerToken) {
throw new Error(
'AUDIUS_API_KEY and AUDIUS_BEARER_TOKEN must be set on the server.'
)
}
serverSdk = createSdkWithServices({
apiKey,
bearerToken,
appName: 'AudiusTrackMigration'
})
return serverSdk
}
34 changes: 34 additions & 0 deletions packages/migrate-tool/api/_lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { VercelRequest, VercelResponse } from '@vercel/node'

/**
* Constant-time check that the incoming Authorization header matches the
* admin bearer token. Returns true if authorized, otherwise writes a 401
* response and returns false — callers can early-return.
*/
export function requireAdmin(
req: VercelRequest,
res: VercelResponse
): boolean {
const expected = process.env.ADMIN_BEARER_TOKEN
if (!expected) {
res.status(500).json({ error: 'ADMIN_BEARER_TOKEN not configured.' })
return false
}
const header = req.headers.authorization ?? ''
const match = /^Bearer\s+(.+)$/.exec(header)
const token = match?.[1] ?? ''
if (!constantTimeEquals(token, expected)) {
res.status(401).json({ error: 'Unauthorized.' })
return false
}
return true
}

function constantTimeEquals(a: string, b: string): boolean {
if (a.length !== b.length) return false
let mismatch = 0
for (let i = 0; i < a.length; i++) {
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i)
}
return mismatch === 0
}
Loading