@open-mercato/shared 0.4.11-develop.1530.964c5df5df → 0.4.11-develop.1532.54adc3ccbf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,8 +21,16 @@ function verifyJwt(token, secret = process.env.JWT_SECRET) {
21
21
  const [h, p, s] = parts;
22
22
  const data = `${h}.${p}`;
23
23
  const expected = base64url(crypto.createHmac("sha256", secret).update(data).digest());
24
- if (!crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))) return null;
25
- const payload = JSON.parse(Buffer.from(p, "base64").toString("utf8"));
24
+ const providedSignature = Buffer.from(s);
25
+ const expectedSignature = Buffer.from(expected);
26
+ if (providedSignature.length !== expectedSignature.length) return null;
27
+ if (!crypto.timingSafeEqual(providedSignature, expectedSignature)) return null;
28
+ let payload;
29
+ try {
30
+ payload = JSON.parse(Buffer.from(p, "base64").toString("utf8"));
31
+ } catch {
32
+ return null;
33
+ }
26
34
  const now = Math.floor(Date.now() / 1e3);
27
35
  if (payload.exp && now > payload.exp) return null;
28
36
  return payload;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/auth/jwt.ts"],
4
- "sourcesContent": ["import crypto from 'node:crypto'\n\nfunction base64url(input: Buffer | string) {\n return (typeof input === 'string' ? Buffer.from(input) : input)\n .toString('base64')\n .replace(/=/g, '')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n}\n\nexport type JwtPayload = Record<string, any>\n\nexport function signJwt(payload: JwtPayload, secret = process.env.JWT_SECRET!, expiresInSec = 60 * 60 * 8) {\n if (!secret) throw new Error('JWT_SECRET is not set')\n const header = { alg: 'HS256', typ: 'JWT' }\n const now = Math.floor(Date.now() / 1000)\n const body = { iat: now, exp: now + expiresInSec, ...payload }\n const encHeader = base64url(JSON.stringify(header))\n const encBody = base64url(JSON.stringify(body))\n const data = `${encHeader}.${encBody}`\n const sig = crypto.createHmac('sha256', secret).update(data).digest()\n const encSig = base64url(sig)\n return `${data}.${encSig}`\n}\n\nexport function verifyJwt(token: string, secret = process.env.JWT_SECRET!) {\n if (!secret) throw new Error('JWT_SECRET is not set')\n const parts = token.split('.')\n if (parts.length !== 3) return null\n const [h, p, s] = parts\n const data = `${h}.${p}`\n const expected = base64url(crypto.createHmac('sha256', secret).update(data).digest())\n if (!crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))) return null\n const payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))\n const now = Math.floor(Date.now() / 1000)\n if (payload.exp && now > payload.exp) return null\n return payload\n}\n\n"],
5
- "mappings": "AAAA,OAAO,YAAY;AAEnB,SAAS,UAAU,OAAwB;AACzC,UAAQ,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,OACtD,SAAS,QAAQ,EACjB,QAAQ,MAAM,EAAE,EAChB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG;AACvB;AAIO,SAAS,QAAQ,SAAqB,SAAS,QAAQ,IAAI,YAAa,eAAe,KAAK,KAAK,GAAG;AACzG,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,QAAM,SAAS,EAAE,KAAK,SAAS,KAAK,MAAM;AAC1C,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,OAAO,EAAE,KAAK,KAAK,KAAK,MAAM,cAAc,GAAG,QAAQ;AAC7D,QAAM,YAAY,UAAU,KAAK,UAAU,MAAM,CAAC;AAClD,QAAM,UAAU,UAAU,KAAK,UAAU,IAAI,CAAC;AAC9C,QAAM,OAAO,GAAG,SAAS,IAAI,OAAO;AACpC,QAAM,MAAM,OAAO,WAAW,UAAU,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO;AACpE,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO,GAAG,IAAI,IAAI,MAAM;AAC1B;AAEO,SAAS,UAAU,OAAe,SAAS,QAAQ,IAAI,YAAa;AACzE,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,QAAM,OAAO,GAAG,CAAC,IAAI,CAAC;AACtB,QAAM,WAAW,UAAU,OAAO,WAAW,UAAU,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO,CAAC;AACpF,MAAI,CAAC,OAAO,gBAAgB,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,QAAQ,CAAC,EAAG,QAAO;AAC3E,QAAM,UAAU,KAAK,MAAM,OAAO,KAAK,GAAG,QAAQ,EAAE,SAAS,MAAM,CAAC;AACpE,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,MAAI,QAAQ,OAAO,MAAM,QAAQ,IAAK,QAAO;AAC7C,SAAO;AACT;",
4
+ "sourcesContent": ["import crypto from 'node:crypto'\n\nfunction base64url(input: Buffer | string) {\n return (typeof input === 'string' ? Buffer.from(input) : input)\n .toString('base64')\n .replace(/=/g, '')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n}\n\nexport type JwtPayload = Record<string, any>\n\nexport function signJwt(payload: JwtPayload, secret = process.env.JWT_SECRET!, expiresInSec = 60 * 60 * 8) {\n if (!secret) throw new Error('JWT_SECRET is not set')\n const header = { alg: 'HS256', typ: 'JWT' }\n const now = Math.floor(Date.now() / 1000)\n const body = { iat: now, exp: now + expiresInSec, ...payload }\n const encHeader = base64url(JSON.stringify(header))\n const encBody = base64url(JSON.stringify(body))\n const data = `${encHeader}.${encBody}`\n const sig = crypto.createHmac('sha256', secret).update(data).digest()\n const encSig = base64url(sig)\n return `${data}.${encSig}`\n}\n\nexport function verifyJwt(token: string, secret = process.env.JWT_SECRET!) {\n if (!secret) throw new Error('JWT_SECRET is not set')\n const parts = token.split('.')\n if (parts.length !== 3) return null\n const [h, p, s] = parts\n const data = `${h}.${p}`\n const expected = base64url(crypto.createHmac('sha256', secret).update(data).digest())\n const providedSignature = Buffer.from(s)\n const expectedSignature = Buffer.from(expected)\n if (providedSignature.length !== expectedSignature.length) return null\n if (!crypto.timingSafeEqual(providedSignature, expectedSignature)) return null\n let payload: JwtPayload\n try {\n payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))\n } catch {\n return null\n }\n const now = Math.floor(Date.now() / 1000)\n if (payload.exp && now > payload.exp) return null\n return payload\n}\n"],
5
+ "mappings": "AAAA,OAAO,YAAY;AAEnB,SAAS,UAAU,OAAwB;AACzC,UAAQ,OAAO,UAAU,WAAW,OAAO,KAAK,KAAK,IAAI,OACtD,SAAS,QAAQ,EACjB,QAAQ,MAAM,EAAE,EAChB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG;AACvB;AAIO,SAAS,QAAQ,SAAqB,SAAS,QAAQ,IAAI,YAAa,eAAe,KAAK,KAAK,GAAG;AACzG,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,QAAM,SAAS,EAAE,KAAK,SAAS,KAAK,MAAM;AAC1C,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,QAAM,OAAO,EAAE,KAAK,KAAK,KAAK,MAAM,cAAc,GAAG,QAAQ;AAC7D,QAAM,YAAY,UAAU,KAAK,UAAU,MAAM,CAAC;AAClD,QAAM,UAAU,UAAU,KAAK,UAAU,IAAI,CAAC;AAC9C,QAAM,OAAO,GAAG,SAAS,IAAI,OAAO;AACpC,QAAM,MAAM,OAAO,WAAW,UAAU,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO;AACpE,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO,GAAG,IAAI,IAAI,MAAM;AAC1B;AAEO,SAAS,UAAU,OAAe,SAAS,QAAQ,IAAI,YAAa;AACzE,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,uBAAuB;AACpD,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,QAAM,OAAO,GAAG,CAAC,IAAI,CAAC;AACtB,QAAM,WAAW,UAAU,OAAO,WAAW,UAAU,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO,CAAC;AACpF,QAAM,oBAAoB,OAAO,KAAK,CAAC;AACvC,QAAM,oBAAoB,OAAO,KAAK,QAAQ;AAC9C,MAAI,kBAAkB,WAAW,kBAAkB,OAAQ,QAAO;AAClE,MAAI,CAAC,OAAO,gBAAgB,mBAAmB,iBAAiB,EAAG,QAAO;AAC1E,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,OAAO,KAAK,GAAG,QAAQ,EAAE,SAAS,MAAM,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,MAAI,QAAQ,OAAO,MAAM,QAAQ,IAAK,QAAO;AAC7C,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.4.11-develop.1530.964c5df5df";
1
+ const APP_VERSION = "0.4.11-develop.1532.54adc3ccbf";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.11-develop.1530.964c5df5df'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.4.11-develop.1532.54adc3ccbf'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.4.11-develop.1530.964c5df5df",
3
+ "version": "0.4.11-develop.1532.54adc3ccbf",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -0,0 +1,81 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ import { signJwt, verifyJwt } from '../jwt'
4
+
5
+ function base64url(input: Buffer | string): string {
6
+ return (typeof input === 'string' ? Buffer.from(input) : input)
7
+ .toString('base64')
8
+ .replace(/=/g, '')
9
+ .replace(/\+/g, '-')
10
+ .replace(/\//g, '_')
11
+ }
12
+
13
+ function signTokenParts(header: string, payload: string, secret: string): string {
14
+ return base64url(crypto.createHmac('sha256', secret).update(`${header}.${payload}`).digest())
15
+ }
16
+
17
+ describe('jwt helpers', () => {
18
+ const secret = 'test-secret'
19
+ const now = new Date('2026-04-11T12:00:00.000Z')
20
+
21
+ beforeEach(() => {
22
+ jest.spyOn(Date, 'now').mockReturnValue(now.getTime())
23
+ })
24
+
25
+ afterEach(() => {
26
+ jest.restoreAllMocks()
27
+ })
28
+
29
+ it('signs and verifies payloads with issued and expiry timestamps', () => {
30
+ const token = signJwt({ sub: 'user-1', roles: ['admin'] }, secret, 300)
31
+
32
+ expect(verifyJwt(token, secret)).toEqual({
33
+ sub: 'user-1',
34
+ roles: ['admin'],
35
+ iat: Math.floor(now.getTime() / 1000),
36
+ exp: Math.floor(now.getTime() / 1000) + 300,
37
+ })
38
+ })
39
+
40
+ it('rejects tokens with tampered payloads', () => {
41
+ const token = signJwt({ sub: 'user-1' }, secret, 300)
42
+ const [header, , signature] = token.split('.')
43
+ const tamperedPayload = base64url(
44
+ JSON.stringify({
45
+ sub: 'user-2',
46
+ iat: Math.floor(now.getTime() / 1000),
47
+ exp: Math.floor(now.getTime() / 1000) + 300,
48
+ })
49
+ )
50
+
51
+ expect(verifyJwt(`${header}.${tamperedPayload}.${signature}`, secret)).toBeNull()
52
+ })
53
+
54
+ it('rejects expired tokens', () => {
55
+ const token = signJwt({ sub: 'user-1' }, secret, 1)
56
+
57
+ jest.spyOn(Date, 'now').mockReturnValue(now.getTime() + 3_000)
58
+
59
+ expect(verifyJwt(token, secret)).toBeNull()
60
+ })
61
+
62
+ it('returns null for malformed signatures', () => {
63
+ const token = signJwt({ sub: 'user-1' }, secret, 300)
64
+ const [header, payload] = token.split('.')
65
+
66
+ expect(verifyJwt(`${header}.${payload}.x`, secret)).toBeNull()
67
+ })
68
+
69
+ it('returns null for signed payloads that are not valid JSON', () => {
70
+ const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
71
+ const payload = base64url('not-json')
72
+ const signature = signTokenParts(header, payload, secret)
73
+
74
+ expect(verifyJwt(`${header}.${payload}.${signature}`, secret)).toBeNull()
75
+ })
76
+
77
+ it('throws when the JWT secret is missing', () => {
78
+ expect(() => signJwt({ sub: 'user-1' }, '')).toThrow('JWT_SECRET is not set')
79
+ expect(() => verifyJwt('header.payload.signature', '')).toThrow('JWT_SECRET is not set')
80
+ })
81
+ })
@@ -30,10 +30,17 @@ export function verifyJwt(token: string, secret = process.env.JWT_SECRET!) {
30
30
  const [h, p, s] = parts
31
31
  const data = `${h}.${p}`
32
32
  const expected = base64url(crypto.createHmac('sha256', secret).update(data).digest())
33
- if (!crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))) return null
34
- const payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))
33
+ const providedSignature = Buffer.from(s)
34
+ const expectedSignature = Buffer.from(expected)
35
+ if (providedSignature.length !== expectedSignature.length) return null
36
+ if (!crypto.timingSafeEqual(providedSignature, expectedSignature)) return null
37
+ let payload: JwtPayload
38
+ try {
39
+ payload = JSON.parse(Buffer.from(p, 'base64').toString('utf8'))
40
+ } catch {
41
+ return null
42
+ }
35
43
  const now = Math.floor(Date.now() / 1000)
36
44
  if (payload.exp && now > payload.exp) return null
37
45
  return payload
38
46
  }
39
-