Repro (4 lines, no Fastify, no MySQL)
import jwt from "jsonwebtoken";
import { readFileSync } from "fs";
const PEM = readFileSync("/tmp/skelpo-jwt.pem", "utf8");
const TOK = jwt.sign({ sub: "u-001", acc: "a-001" }, PEM, { algorithm: "ES256", issuer: "z" });
const decoded = jwt.verify(TOK, PEM, { algorithms: ["ES256"], issuer: "z" }) as any;
console.log("typeof decoded:", typeof decoded);
console.log("decoded JSON:", JSON.stringify(decoded));
console.log("decoded.sub:", decoded.sub);
Observed (perry 0.5.958)
[jwt-verify] success, claims={"acc":"a-001","sub":"u-001"} ← binding decoded correctly
typeof decoded: string ← but returned as STRING
decoded JSON: "{\"acc\":\"a-001\",\"sub\":\"u-001\"}" ← JSON-encoded-string-of-the-object
decoded.sub: undefined ← property access yields undefined
Expected (tsx / node — same source)
typeof decoded: object
decoded JSON: {"sub":"u-001","acc":"a-001","iat":..., "iss":"z"}
decoded.sub: u-001
Per spec / jsonwebtoken README, jwt.verify returns the decoded payload as an object (it's the JwtPayload type). Perry's binding decodes correctly internally (the [jwt-verify] success log shows the right shape) but the value returned to user code is the JSON-stringified object, not the parsed one.
How this breaks shop-admin
server/crypto/jwt.ts::verifyAccessToken does:
const decoded = jwt.verify(token, loadKey(), { algorithms: [ALG], issuer: ISSUER }) as jwt.JwtPayload;
if (typeof decoded.sub !== "string" || typeof decoded["acc"] !== "string" || ...) {
return null;
}
return { sub: decoded.sub, acc: decoded["acc"] as string, ... };
Under perry: decoded is a string. decoded.sub is undefined. typeof undefined !== "string" is true → returns null. The auth middleware then early-returns without setting req.user, every authenticated route sees req.user === undefined, and requireUser(req) throws — which on its own triggers a second perry bug (issue to be filed separately): throw new Error/ApiError in an async route handler crashes the process instead of being caught by Fastify.
This is the residual blocker for the shop-admin native server after #859 and #915 closed; signup itself now works end-to-end (HTTP 201 with full JWT JSON, all 5 tables populate) but the very first authenticated request fails because of this jwt.verify return-shape bug.
Pointer
Likely in the perry-stdlib jsonwebtoken binding. The fix presumably is to return the parsed payload object directly to the JS side rather than the JSON-encoded representation; the js_jsonwebtoken_verify (or whatever wrapper) needs to construct a JS object from the verified payload rather than handing back the JSON text.
jwt.sign works correctly (verified by my #915 minimal repro and the standalone working).
Files: this repro is short enough that nothing's committed to the user repo for it; happy to PR a test fixture into test-files/ if useful.
Local checkout: clean at perry HEAD (v0.5.958), no source edits.
Repro (4 lines, no Fastify, no MySQL)
Observed (perry 0.5.958)
Expected (tsx / node — same source)
Per spec /
jsonwebtokenREADME,jwt.verifyreturns the decoded payload as an object (it's theJwtPayloadtype). Perry's binding decodes correctly internally (the[jwt-verify] successlog shows the right shape) but the value returned to user code is the JSON-stringified object, not the parsed one.How this breaks shop-admin
server/crypto/jwt.ts::verifyAccessTokendoes:Under perry:
decodedis a string.decoded.subisundefined.typeof undefined !== "string"istrue→ returnsnull. The auth middleware then early-returns without settingreq.user, every authenticated route seesreq.user === undefined, andrequireUser(req)throws — which on its own triggers a second perry bug (issue to be filed separately):throw new Error/ApiErrorin an async route handler crashes the process instead of being caught by Fastify.This is the residual blocker for the shop-admin native server after #859 and #915 closed; signup itself now works end-to-end (HTTP 201 with full JWT JSON, all 5 tables populate) but the very first authenticated request fails because of this
jwt.verifyreturn-shape bug.Pointer
Likely in the perry-stdlib
jsonwebtokenbinding. The fix presumably is to return the parsed payload object directly to the JS side rather than the JSON-encoded representation; thejs_jsonwebtoken_verify(or whatever wrapper) needs to construct a JS object from the verified payload rather than handing back the JSON text.jwt.signworks correctly (verified by my #915 minimal repro and the standalone working).Files: this repro is short enough that nothing's committed to the user repo for it; happy to PR a test fixture into
test-files/if useful.Local checkout: clean at perry HEAD (v0.5.958), no source edits.