HeyPuter/puter · error · HttpError
invalid_token
invalid_token
Error message
wrong token type
What it means
Thrown by POST /wisp/relay-token/verify when the token's signature verifies under the 'wisp' purpose but its `$` discriminator field is not the literal `token:wisp`. Every wisp relay token carries this type tag so a token minted for another purpose can't be replayed here. IMPORTANT: this throw lives inside a try/catch whose catch unconditionally rethrows a generic 'Forbidden' (line 126), so a client never actually receives this message — it is always masked into error 301's 'Forbidden'. Treat it as an internal/defensive guard, not a user-facing surface.
Source
Thrown at src/backend/controllers/wisp/WispController.ts:122
};
/** POST /wisp/relay-token/verify — verify a relay token and apply policy. */
#verify = async (req: Request, res: Response): Promise<void> => {
const bodyToken = req.body?.token;
if (!bodyToken || typeof bodyToken !== 'string') {
throw new HttpError(400, 'Missing `token`', {
legacyCode: 'token_missing',
});
}
let decoded: Record<string, unknown>;
try {
decoded = this.services.token.verify<Record<string, unknown>>(
'wisp',
bodyToken,
);
if (decoded.$ !== 'token:wisp')
throw new HttpError(403, 'wrong token type', {
legacyCode: 'invalid_token',
});
} catch {
throw new HttpError(403, 'Forbidden', {
legacyCode: 'invalid_token',
});
}
// Build policy event — extensions can deny via extension.on('wisp.get-policy')
const isGuest = Boolean(decoded.guest);
let user: Record<string, unknown> | null = null;
if (!isGuest && decoded.user_uid) {
user = await this.stores.user.getByUuid(String(decoded.user_uid));
}
const event: Record<string, unknown> = {
allow: true,
policy: { allow: true },View on GitHub (pinned to 908ec23eda)
Solutions
- Fetch a fresh relay token from POST /wisp/relay-token — the mint endpoint always sets `$: 'token:wisp'`.
- If you mint wisp tokens yourself, ensure the payload includes `$: 'token:wisp'`.
- Do not reuse tokens obtained from other endpoints on the verify route.
Example fix
// before (client reuses a foreign token)
verify({ token: someOtherToken });
// after (fetch a fresh wisp relay token first)
const { token } = await fetch('/wisp/relay-token', { method: 'POST' }).then(r => r.json());
verify({ token }); Defensive patterns
Strategy: validation
Validate before calling
// If you mint wisp tokens yourself, validate the discriminator before sending:
function isWispRelayTokenPayload(d) {
return d != null && typeof d === 'object' && d['$'] === 'token:wisp';
}
if (!isWispRelayTokenPayload(decodedPayload)) {
throw new Error('not a wisp relay token — refetch from /wisp/relay-token');
} Type guard
const isWispTokenPayload = (d) => d != null && typeof d === 'object' && d['$'] === 'token:wisp';
Try / catch
// Note: this message is masked by the surrounding catch (see error 301). // Clients always receive the generic 'Forbidden' — handle it there.
Prevention
- Always obtain wisp relay tokens from POST /wisp/relay-token rather than reusing or hand-building them.
- If you mint tokens, always set the `$: 'token:wisp'` payload field.
- Don't reuse tokens from other endpoints on the verify route.
When it happens
Trigger: POST /wisp/relay-token/verify with a token that is a valid wisp-secret-signed JWT but whose payload lacks `$` or sets `$` to something other than `token:wisp` (e.g. a hand-built token, or one minted by an older code path that did not set the tag).
Common situations: Token format version mismatch after an upgrade; a client reusing a token minted by a different endpoint that happens to share the wisp secret; test fixtures that build tokens manually without the `$` field.
Related errors
- token_auth_failed
- token_missing
- app_or_api_token_required
- App not found
- Invalid orderBy. Allowed: ${ALLOWED_ORDER_BY.join(', ')}
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/b88969a900e4d6e7.
Report an issue: GitHub.