W005: Create API token (PAT)
Generates a Personal Access Token (PAT) for programmatic API access — CLIs, AI agents, third-party integrations. The plaintext token is returned once in the response; the backend stores only the SHA-256 hash. Lost plaintext → revoke (W007) and create a new one.
Token format: pharus_pat_<32 base62 chars> (43 chars). The branded prefix is meaningful to two consumers:
- Auth middleware routes the bearer between PAT verification (this token) and Firebase ID-token verification.
- Secret scanners (e.g. GitHub on push) flag accidentally committed tokens by prefix.
Steps
- Validate input. Trim the name; reject empty.
- Validate org binding (if provided). When
orgIdis non-null, callPlatform.getMembershipto confirm the caller is an active member. Return 403 otherwise. - Compute expiry. When
expiresInDays > 0, setexpiresAt = now + days. Otherwise null. - Generate plaintext.
crypto.randomBytes(32)→ base62-encode → prependpharus_pat_. - Insert the row. Call
Platform.createApiTokenwith{ userId, orgId, name, tokenPrefix (first 12 chars), tokenHash (SHA-256), expiresAt }. - Return the row + the plaintext. The plaintext is never persisted server-side.
Returns
{ token: ApiToken, plaintext: string }.
Business rules
- Plaintext shown once. The response is the only place the full token appears. Subsequent reads via W006 only return prefix + metadata.
- Org binding overrides X-Org-Id. A PAT with
orgIdset ignores the request header on tenant routes; an unscoped PAT still requires the header. - No scopes. v1 PATs have a single permission: "full access as user". Fine-grained scopes are a future enhancement.
Errors
ForbiddenError— no userId on ctx (shouldn't happen — middleware enforces).ForbiddenError—orgIdprovided and caller is not a member.ValidationError— empty name.