migration(0004): backfill workspace_token kind for cloud-agent-box keys
Summary
The pre-#42 /v1/tokens/workspace route in @relayauth/server@0.2.9 stored rows in api_keys with kind='api_key' and workspace_id=NULL even when the caller requested a workspace token. The key value carries the relay_ws_ prefix and is accepted by the apiKeyAuth middleware, but resolveWorkspaceToken (used by /v1/tokens/path) rejects them because kind != 'workspace_token' — surfacing as 401 workspace_token_required in the cloud-agent box-warm flow.
The route fix that correctly sets kind/workspace_id at mint time is already in main via #42 (feat(tokens): implement path-scoped relayfile tokens). This migration cleans up rows that were stored incorrectly under the old route so they resolve as workspace tokens once the new code is deployed.
Scope
UPDATE api_keys
SET kind = 'workspace_token',
workspace_id = SUBSTR(name, LENGTH('cloud-agent-box:') + 1)
WHERE name LIKE 'cloud-agent-box:%'
AND kind = 'api_key'
AND revoked_at IS NULL;
Only rows matching the documented cloud-agent-box:<workspaceId> shape (cloud-web-worker mint pattern, see cloud:packages/web/lib/relay-workspaces.ts:mintRelayAuthWorkspaceToken). Idempotent — the kind='api_key' AND revoked_at IS NULL guards make re-runs harmless.
How this surfaces
Pear's cloud-agent attach flow (cloud-web-worker → relayauth /v1/tokens/workspace → relayauth /v1/tokens/path) was failing with workspace_token_required. Manual D1 patches in prod confirmed: once these rows are corrected (and the new route code is deployed), the box-warm chain completes and Pear attaches.
Release / deploy chain
- ✅ This migration merged into relayauth main.
- Publish a new
@relayauth/serverrelease (e.g.0.3.0) including this migration + the #42 route fix. - Bump
@relayauth/serverinAgentWorkforce/cloud(follow-up PR). - Cloud deploy → runs new migrations against
relayauthD1 in each stage + deploys the worker with the kind-correct route. New mints land withkind='workspace_token'directly; this migration only catches the leftover bad rows.
🤖 Generated with Claude Code