{"record":{"id":"b88969a900e4d6e7","repo":"HeyPuter/puter","slug":"invalid-token","errorCode":"invalid_token","errorMessage":"wrong token type","messagePattern":"wrong token type","errorType":"exception","errorClass":"HttpError","httpStatus":403,"severity":"error","filePath":"src/backend/controllers/wisp/WispController.ts","lineNumber":122,"sourceCode":"    };\n\n    /** POST /wisp/relay-token/verify — verify a relay token and apply policy. */\n    #verify = async (req: Request, res: Response): Promise<void> => {\n        const bodyToken = req.body?.token;\n        if (!bodyToken || typeof bodyToken !== 'string') {\n            throw new HttpError(400, 'Missing `token`', {\n                legacyCode: 'token_missing',\n            });\n        }\n\n        let decoded: Record<string, unknown>;\n        try {\n            decoded = this.services.token.verify<Record<string, unknown>>(\n                'wisp',\n                bodyToken,\n            );\n            if (decoded.$ !== 'token:wisp')\n                throw new HttpError(403, 'wrong token type', {\n                    legacyCode: 'invalid_token',\n                });\n        } catch {\n            throw new HttpError(403, 'Forbidden', {\n                legacyCode: 'invalid_token',\n            });\n        }\n\n        // Build policy event — extensions can deny via extension.on('wisp.get-policy')\n        const isGuest = Boolean(decoded.guest);\n        let user: Record<string, unknown> | null = null;\n        if (!isGuest && decoded.user_uid) {\n            user = await this.stores.user.getByUuid(String(decoded.user_uid));\n        }\n\n        const event: Record<string, unknown> = {\n            allow: true,\n            policy: { allow: true },","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/controllers/wisp/WispController.ts#L104-L140","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before (client reuses a foreign token)\nverify({ token: someOtherToken });\n// after (fetch a fresh wisp relay token first)\nconst { token } = await fetch('/wisp/relay-token', { method: 'POST' }).then(r => r.json());\nverify({ token });","handlingStrategy":"validation","validationCode":"// If you mint wisp tokens yourself, validate the discriminator before sending:\nfunction isWispRelayTokenPayload(d) {\n  return d != null && typeof d === 'object' && d['$'] === 'token:wisp';\n}\nif (!isWispRelayTokenPayload(decodedPayload)) {\n  throw new Error('not a wisp relay token — refetch from /wisp/relay-token');\n}","typeGuard":"const isWispTokenPayload = (d) => d != null && typeof d === 'object' && d['$'] === 'token:wisp';","tryCatchPattern":"// Note: this message is masked by the surrounding catch (see error 301).\n// Clients always receive the generic 'Forbidden' — handle it there.","preventionTips":["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."],"tags":["auth","token","wisp","type-tag"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}