Sample
jsonwebtoken 9.0.3: Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units
Verified sample for npm jsonwebtoken 9.0.3: Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units. The contract…
sha256:db08d34218a323680c118afa134f755bba43d7cabe50ec8738997d1c4a5b68f0
This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured.
How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people.
MIT-0
Execution evidence
The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.
- Evidence basis
- Signed contract pass
- Verification receipts
- 2
- Signing keys that built it
- 2
Declared environment
node linux x64 node node npm
Verification-run environments
| Environment | Contract | Stages | Run |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-16 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
Case
HOW- Goal
- Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units
- Packages
- Symbols
-
- jwt.sign
- jwt.verify
- Environment
- node
- Created
- 2026-08-16T12:10:28Z
Contract
- assert jwt.verify returns payload claims directly at root and leaves .payload undefined unless complete: true is specified
- assert jwt.verify with complete: true returns an envelope containing header, payload, and signature
- assert jwt.sign throws a runtime error when registered claims like exp, aud, or iss exist in both payload and options
- assert numeric expiresIn is interpreted as relative duration in seconds rather than milliseconds or an absolute timestamp
- assert signing a string payload produces a raw string on verify and rejects expiresIn option
Files
- NOTES.md
- csx.json
- package-lock.json
- package.json
- src/index.mjs
- test/contract.mjs
Source
# jsonwebtoken Verification Envelopes and Signing Claim Rules
## Prior Solutions
A search for `jsonwebtoken` returned existing published sample `sha256:83fd833c641d68652a876b9d95c6f97d220b969dc2a1f2cd2512294b6cfddbb6` ("Verify a JWT without accidentally trusting an unsigned token"), which covers pinning algorithms to reject unsigned tokens and prevent algorithm-confusion vulnerabilities. This sample investigates a different set of API traps: return value envelope shape differences between default and complete modes, registered claim duplication errors between payload objects and options, and duration unit interpretations in `jwt.sign`.
## Failure Mode
Code written assuming `jwt.verify` returns a nested envelope `{ payload, header }` fails loudly at runtime with `TypeError: Cannot read properties of undefined` when attempting to access `decoded.payload.sub`. Similarly, specifying registered claims like `exp` or `aud` in both the payload object and the options parameter fails loudly at runtime with `Error: Bad "options.expiresIn" option the payload already has an "exp" property.`
{"case":{"believed":"jwt.verify returns a decoded envelope containing payload and header properties by default, and jwt.sign merges payload registered claims with signing options.","caseId":"case:sha256:a1e359b04d2db6b1654df36689f3d6c8745619feac4aec9569a1338caa6eb5bb","contract":["assert jwt.verify returns payload claims directly at root and leaves .payload undefined unless complete: true is specified","assert jwt.verify with complete: true returns an envelope containing header, payload, and signature","assert jwt.sign throws a runtime error when registered claims like exp, aud, or iss exist in both payload and options","assert numeric expiresIn is interpreted as relative duration in seconds rather than milliseconds or an absolute timestamp","assert signing a string payload produces a raw string on verify and rejects expiresIn option"],"goal":"Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units","kind":"HOW","packages":["pkg:npm/jsonwebtoken@9.0.3"],"schemaVersion":1,"symbols":["jwt.sign","jwt.verify"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/jsonwebtoken@9.0.3"],"schemaVersion":1,"symbols":["jwt.sign","jwt.verify"],"verifierAdapter":"node-typescript@1"}
{
"name": "csx-sample-jsonwebtoken-payload-trap",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "csx-sample-jsonwebtoken-payload-trap",
"version": "1.0.0",
"license": "MIT-0",
"dependencies": {
"jsonwebtoken": "^9.0.2"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
}
}
}
{
"name": "jsonwebtoken-api-contract",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT-0",
"dependencies": {
"jsonwebtoken": "9.0.3"
}
}
import jwt from 'jsonwebtoken';
/**
* Signs a payload with HS256 algorithm.
* Demonstrates proper claim separation: registered claims should be passed via options
* or directly in the payload, but never duplicated across both.
*/
export function signToken(payload, secret, options = {}) {
return jwt.sign(payload, secret, {
algorithm: 'HS256',
...options
});
}
/**
* Verifies a token with HS256 algorithm.
* By default returns the decoded payload directly.
* When options.complete is true, returns { header, payload, signature }.
*/
export function verifyToken(token, secret, options = {}) {
return jwt.verify(token, secret, {
algorithms: ['HS256'],
...options
});
}
import { strict as assert } from 'node:assert';
import { signToken, verifyToken } from '../src/index.mjs';
const secret = 'super-secret-contract-key-2026';
// 1. jwt.verify returns the payload claims directly at root, not wrapped in a .payload object.
const tokenWithHeader = signToken(
{ sub: 'user-42', role: 'admin' },
secret,
{
expiresIn: '1h',
header: { kid: 'auth-key-1' }
}
);
const decodedDirect = verifyToken(tokenWithHeader, secret);
assert.equal(decodedDirect.sub, 'user-42');
assert.equal(decodedDirect.role, 'admin');
assert.equal(decodedDirect.payload, undefined);
assert.equal(decodedDirect.header, undefined);
// 2. Accessing the header or full envelope requires explicit { complete: true }.
const decodedComplete = verifyToken(tokenWithHeader, secret, { complete: true });
assert.equal(decodedComplete.header.kid, 'auth-key-1');
assert.equal(decodedComplete.header.alg, 'HS256');
assert.equal(decodedComplete.payload.sub, 'user-42');
assert.equal(typeof decodedComplete.signature, 'string');
// 3. Duplicate registered claims between payload and options throw fatal runtime errors.
assert.throws(
() => signToken({ sub: 'user-42', exp: Math.floor(Date.now() / 1000) + 3600 }, secret, { expiresIn: '1h' }),
/Bad "options\.expiresIn" option the payload already has an "exp" property\./
);
assert.throws(
() => signToken({ sub: 'user-42', aud: 'https://api.example.com' }, secret, { audience: 'https://api.example.com' }),
/Bad "options\.audience" option\. The payload already has an "aud" property\./
);
assert.throws(
() => signToken({ sub: 'user-42', iss: 'https://auth.example.com' }, secret, { issuer: 'https://auth.example.com' }),
/Bad "options\.issuer" option\. The payload already has an "iss" property\./
);
// 4. Numeric expiresIn is relative seconds from issued-at, not milliseconds or a unix timestamp.
const numericTtlToken = signToken({ sub: 'user-42' }, secret, { expiresIn: 120 });
const decodedNumeric = verifyToken(numericTtlToken, secret);
assert.equal(decodedNumeric.exp - decodedNumeric.iat, 120);
// 5. String payloads are signed as raw data: they reject expiresIn and verify back to a raw string primitive.
const rawStringToken = signToken('plain-text-payload', secret);
const decodedRawString = verifyToken(rawStringToken, secret);
assert.equal(typeof decodedRawString, 'string');
assert.equal(decodedRawString, 'plain-text-payload');
assert.throws(
() => signToken('plain-text-payload', secret, { expiresIn: '1h' }),
/invalid expiresIn option for string payload/
);
console.log('CONTRACT PASS: jsonwebtoken verify returns payload directly, rejects duplicate claims, and handles duration units');