@cmssy/next 0.5.4 → 0.5.5

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.
Files changed (3) hide show
  1. package/dist/index.cjs +122 -96
  2. package/dist/index.js +123 -97
  3. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -4,12 +4,108 @@ var headers = require('next/headers');
4
4
  var navigation = require('next/navigation');
5
5
  var react = require('@cmssy/react');
6
6
  var client = require('@cmssy/react/client');
7
+ var jose = require('jose');
7
8
  var jsxRuntime = require('react/jsx-runtime');
8
9
  var crypto$1 = require('crypto');
9
10
  var server = require('next/server');
10
- var jose = require('jose');
11
11
 
12
12
  // src/create-cmssy-page.tsx
13
+ var CMSSY_SESSION_COOKIE = "cmssy_session";
14
+ var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
15
+ var MIN_SESSION_SECRET_LENGTH = 32;
16
+ var ACCESS_EXPIRY_SKEW_MS = 3e4;
17
+ async function deriveSessionKey(secret) {
18
+ if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
19
+ throw new Error(
20
+ `cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
21
+ );
22
+ }
23
+ const digest = await crypto.subtle.digest(
24
+ "SHA-256",
25
+ new TextEncoder().encode(secret)
26
+ );
27
+ return new Uint8Array(digest);
28
+ }
29
+ async function sealSession(payload, secret, audience) {
30
+ const key = await deriveSessionKey(secret);
31
+ const jwt = new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
32
+ if (audience) jwt.setAudience(audience);
33
+ return jwt.encrypt(key);
34
+ }
35
+ async function openSession(token, secret, audience) {
36
+ const key = await deriveSessionKey(secret);
37
+ try {
38
+ const { payload } = await jose.jwtDecrypt(token, key, {
39
+ keyManagementAlgorithms: ["dir"],
40
+ contentEncryptionAlgorithms: ["A256GCM"],
41
+ ...audience ? { audience } : {}
42
+ });
43
+ const { accessToken, refreshToken, accessExpiresAt, user } = payload;
44
+ if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
45
+ return null;
46
+ }
47
+ return {
48
+ accessToken,
49
+ refreshToken,
50
+ accessExpiresAt,
51
+ user: {
52
+ recordId: user.recordId,
53
+ email: user.email
54
+ }
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ function isAccessExpired(payload, now = Date.now()) {
61
+ return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
62
+ }
63
+ function sessionCookieOptions() {
64
+ return {
65
+ httpOnly: true,
66
+ secure: process.env.NODE_ENV !== "development",
67
+ sameSite: "lax",
68
+ path: "/",
69
+ maxAge: SESSION_MAX_AGE_SECONDS
70
+ };
71
+ }
72
+
73
+ // src/config.ts
74
+ function assertAuthConfig(config) {
75
+ const auth = config.auth;
76
+ if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
77
+ throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
78
+ }
79
+ if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
80
+ throw new Error(
81
+ `cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
82
+ );
83
+ }
84
+ return auth;
85
+ }
86
+
87
+ // src/auth-server.ts
88
+ async function readValidSession(config) {
89
+ const auth = assertAuthConfig(config);
90
+ const jar = await headers.cookies();
91
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
92
+ if (!raw) return null;
93
+ const session = await openSession(
94
+ raw,
95
+ auth.sessionSecret,
96
+ config.workspaceSlug
97
+ );
98
+ if (!session || isAccessExpired(session)) return null;
99
+ return session;
100
+ }
101
+ async function getCmssyUser(config) {
102
+ const session = await readValidSession(config);
103
+ return session?.user ?? null;
104
+ }
105
+ async function getCmssyAccessToken(config) {
106
+ const session = await readValidSession(config);
107
+ return session?.accessToken ?? null;
108
+ }
13
109
 
14
110
  // src/csp.ts
15
111
  function toCspOrigin(origin) {
@@ -70,6 +166,7 @@ function createCmssyPage(config, blocks, options) {
70
166
  apiUrl: config.apiUrl,
71
167
  workspaceSlug: config.workspaceSlug
72
168
  };
169
+ const client$1 = react.createCmssyClient(clientConfig);
73
170
  return async function CmssyCatchAllPage({
74
171
  params,
75
172
  searchParams
@@ -131,6 +228,27 @@ function createCmssyPage(config, blocks, options) {
131
228
  }
132
229
  ) });
133
230
  }
231
+ let auth;
232
+ if (config.auth) {
233
+ try {
234
+ const user = await getCmssyUser(config);
235
+ auth = {
236
+ isAuthenticated: !!user,
237
+ member: user ? { recordId: user.recordId, email: user.email } : null
238
+ };
239
+ } catch {
240
+ auth = void 0;
241
+ }
242
+ }
243
+ let workspace;
244
+ try {
245
+ workspace = {
246
+ id: await client$1.resolveWorkspaceId(),
247
+ slug: config.workspaceSlug
248
+ };
249
+ } catch {
250
+ workspace = void 0;
251
+ }
134
252
  return /* @__PURE__ */ jsxRuntime.jsx(client.CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsxRuntime.jsx(
135
253
  react.CmssyServerPage,
136
254
  {
@@ -139,7 +257,9 @@ function createCmssyPage(config, blocks, options) {
139
257
  locale,
140
258
  defaultLocale,
141
259
  enabledLocales,
142
- forms
260
+ forms,
261
+ auth,
262
+ workspace
143
263
  }
144
264
  ) });
145
265
  };
@@ -509,79 +629,6 @@ function createCmssyLocaleMiddleware(config) {
509
629
  return server.NextResponse.next({ request: { headers: headers3 } });
510
630
  };
511
631
  }
512
- var CMSSY_SESSION_COOKIE = "cmssy_session";
513
- var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
514
- var MIN_SESSION_SECRET_LENGTH = 32;
515
- var ACCESS_EXPIRY_SKEW_MS = 3e4;
516
- async function deriveSessionKey(secret) {
517
- if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
518
- throw new Error(
519
- `cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
520
- );
521
- }
522
- const digest = await crypto.subtle.digest(
523
- "SHA-256",
524
- new TextEncoder().encode(secret)
525
- );
526
- return new Uint8Array(digest);
527
- }
528
- async function sealSession(payload, secret, audience) {
529
- const key = await deriveSessionKey(secret);
530
- const jwt = new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
531
- if (audience) jwt.setAudience(audience);
532
- return jwt.encrypt(key);
533
- }
534
- async function openSession(token, secret, audience) {
535
- const key = await deriveSessionKey(secret);
536
- try {
537
- const { payload } = await jose.jwtDecrypt(token, key, {
538
- keyManagementAlgorithms: ["dir"],
539
- contentEncryptionAlgorithms: ["A256GCM"],
540
- ...audience ? { audience } : {}
541
- });
542
- const { accessToken, refreshToken, accessExpiresAt, user } = payload;
543
- if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
544
- return null;
545
- }
546
- return {
547
- accessToken,
548
- refreshToken,
549
- accessExpiresAt,
550
- user: {
551
- recordId: user.recordId,
552
- email: user.email
553
- }
554
- };
555
- } catch {
556
- return null;
557
- }
558
- }
559
- function isAccessExpired(payload, now = Date.now()) {
560
- return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
561
- }
562
- function sessionCookieOptions() {
563
- return {
564
- httpOnly: true,
565
- secure: process.env.NODE_ENV !== "development",
566
- sameSite: "lax",
567
- path: "/",
568
- maxAge: SESSION_MAX_AGE_SECONDS
569
- };
570
- }
571
-
572
- // src/config.ts
573
- function assertAuthConfig(config) {
574
- const auth = config.auth;
575
- if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
576
- throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
577
- }
578
- if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
579
- throw new Error(
580
- `cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
581
- );
582
- }
583
- return auth;
584
- }
585
632
  var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
586
633
  siteMemberLogin(input: $input) {
587
634
  success message accessToken refreshToken accessTokenExpiresIn
@@ -1289,27 +1336,6 @@ function createCmssyCartRoute(config) {
1289
1336
  }
1290
1337
  };
1291
1338
  }
1292
- async function readValidSession(config) {
1293
- const auth = assertAuthConfig(config);
1294
- const jar = await headers.cookies();
1295
- const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
1296
- if (!raw) return null;
1297
- const session = await openSession(
1298
- raw,
1299
- auth.sessionSecret,
1300
- config.workspaceSlug
1301
- );
1302
- if (!session || isAccessExpired(session)) return null;
1303
- return session;
1304
- }
1305
- async function getCmssyUser(config) {
1306
- const session = await readValidSession(config);
1307
- return session?.user ?? null;
1308
- }
1309
- async function getCmssyAccessToken(config) {
1310
- const session = await readValidSession(config);
1311
- return session?.accessToken ?? null;
1312
- }
1313
1339
  function isPrefetch(request2) {
1314
1340
  return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
1315
1341
  }
package/dist/index.js CHANGED
@@ -1,13 +1,109 @@
1
1
  import { draftMode, headers, cookies } from 'next/headers';
2
2
  import { notFound, redirect } from 'next/navigation';
3
- import { resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
3
+ import { createCmssyClient, resolveSiteLocales, splitLocaleFromPath, fetchPage, resolveForms, CmssyServerPage, fetchSiteConfig, fetchPageById, fetchPages, fetchPageMeta, normalizeSlug as normalizeSlug$1, resolveWorkspaceId, graphqlRequest } from '@cmssy/react';
4
4
  import { CmssyLocaleProvider } from '@cmssy/react/client';
5
+ import { EncryptJWT, jwtDecrypt } from 'jose';
5
6
  import { jsx, jsxs } from 'react/jsx-runtime';
6
7
  import { createHmac, createHash, timingSafeEqual } from 'crypto';
7
8
  import { NextResponse } from 'next/server';
8
- import { EncryptJWT, jwtDecrypt } from 'jose';
9
9
 
10
10
  // src/create-cmssy-page.tsx
11
+ var CMSSY_SESSION_COOKIE = "cmssy_session";
12
+ var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
13
+ var MIN_SESSION_SECRET_LENGTH = 32;
14
+ var ACCESS_EXPIRY_SKEW_MS = 3e4;
15
+ async function deriveSessionKey(secret) {
16
+ if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
17
+ throw new Error(
18
+ `cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
19
+ );
20
+ }
21
+ const digest = await crypto.subtle.digest(
22
+ "SHA-256",
23
+ new TextEncoder().encode(secret)
24
+ );
25
+ return new Uint8Array(digest);
26
+ }
27
+ async function sealSession(payload, secret, audience) {
28
+ const key = await deriveSessionKey(secret);
29
+ const jwt = new EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
30
+ if (audience) jwt.setAudience(audience);
31
+ return jwt.encrypt(key);
32
+ }
33
+ async function openSession(token, secret, audience) {
34
+ const key = await deriveSessionKey(secret);
35
+ try {
36
+ const { payload } = await jwtDecrypt(token, key, {
37
+ keyManagementAlgorithms: ["dir"],
38
+ contentEncryptionAlgorithms: ["A256GCM"],
39
+ ...audience ? { audience } : {}
40
+ });
41
+ const { accessToken, refreshToken, accessExpiresAt, user } = payload;
42
+ if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
43
+ return null;
44
+ }
45
+ return {
46
+ accessToken,
47
+ refreshToken,
48
+ accessExpiresAt,
49
+ user: {
50
+ recordId: user.recordId,
51
+ email: user.email
52
+ }
53
+ };
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function isAccessExpired(payload, now = Date.now()) {
59
+ return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
60
+ }
61
+ function sessionCookieOptions() {
62
+ return {
63
+ httpOnly: true,
64
+ secure: process.env.NODE_ENV !== "development",
65
+ sameSite: "lax",
66
+ path: "/",
67
+ maxAge: SESSION_MAX_AGE_SECONDS
68
+ };
69
+ }
70
+
71
+ // src/config.ts
72
+ function assertAuthConfig(config) {
73
+ const auth = config.auth;
74
+ if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
75
+ throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
76
+ }
77
+ if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
78
+ throw new Error(
79
+ `cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
80
+ );
81
+ }
82
+ return auth;
83
+ }
84
+
85
+ // src/auth-server.ts
86
+ async function readValidSession(config) {
87
+ const auth = assertAuthConfig(config);
88
+ const jar = await cookies();
89
+ const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
90
+ if (!raw) return null;
91
+ const session = await openSession(
92
+ raw,
93
+ auth.sessionSecret,
94
+ config.workspaceSlug
95
+ );
96
+ if (!session || isAccessExpired(session)) return null;
97
+ return session;
98
+ }
99
+ async function getCmssyUser(config) {
100
+ const session = await readValidSession(config);
101
+ return session?.user ?? null;
102
+ }
103
+ async function getCmssyAccessToken(config) {
104
+ const session = await readValidSession(config);
105
+ return session?.accessToken ?? null;
106
+ }
11
107
 
12
108
  // src/csp.ts
13
109
  function toCspOrigin(origin) {
@@ -68,6 +164,7 @@ function createCmssyPage(config, blocks, options) {
68
164
  apiUrl: config.apiUrl,
69
165
  workspaceSlug: config.workspaceSlug
70
166
  };
167
+ const client = createCmssyClient(clientConfig);
71
168
  return async function CmssyCatchAllPage({
72
169
  params,
73
170
  searchParams
@@ -129,6 +226,27 @@ function createCmssyPage(config, blocks, options) {
129
226
  }
130
227
  ) });
131
228
  }
229
+ let auth;
230
+ if (config.auth) {
231
+ try {
232
+ const user = await getCmssyUser(config);
233
+ auth = {
234
+ isAuthenticated: !!user,
235
+ member: user ? { recordId: user.recordId, email: user.email } : null
236
+ };
237
+ } catch {
238
+ auth = void 0;
239
+ }
240
+ }
241
+ let workspace;
242
+ try {
243
+ workspace = {
244
+ id: await client.resolveWorkspaceId(),
245
+ slug: config.workspaceSlug
246
+ };
247
+ } catch {
248
+ workspace = void 0;
249
+ }
132
250
  return /* @__PURE__ */ jsx(CmssyLocaleProvider, { value: localeContext, children: /* @__PURE__ */ jsx(
133
251
  CmssyServerPage,
134
252
  {
@@ -137,7 +255,9 @@ function createCmssyPage(config, blocks, options) {
137
255
  locale,
138
256
  defaultLocale,
139
257
  enabledLocales,
140
- forms
258
+ forms,
259
+ auth,
260
+ workspace
141
261
  }
142
262
  ) });
143
263
  };
@@ -507,79 +627,6 @@ function createCmssyLocaleMiddleware(config) {
507
627
  return NextResponse.next({ request: { headers: headers3 } });
508
628
  };
509
629
  }
510
- var CMSSY_SESSION_COOKIE = "cmssy_session";
511
- var SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
512
- var MIN_SESSION_SECRET_LENGTH = 32;
513
- var ACCESS_EXPIRY_SKEW_MS = 3e4;
514
- async function deriveSessionKey(secret) {
515
- if (typeof secret !== "string" || secret.length < MIN_SESSION_SECRET_LENGTH) {
516
- throw new Error(
517
- `cmssy auth sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
518
- );
519
- }
520
- const digest = await crypto.subtle.digest(
521
- "SHA-256",
522
- new TextEncoder().encode(secret)
523
- );
524
- return new Uint8Array(digest);
525
- }
526
- async function sealSession(payload, secret, audience) {
527
- const key = await deriveSessionKey(secret);
528
- const jwt = new EncryptJWT({ ...payload }).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).setIssuedAt().setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`);
529
- if (audience) jwt.setAudience(audience);
530
- return jwt.encrypt(key);
531
- }
532
- async function openSession(token, secret, audience) {
533
- const key = await deriveSessionKey(secret);
534
- try {
535
- const { payload } = await jwtDecrypt(token, key, {
536
- keyManagementAlgorithms: ["dir"],
537
- contentEncryptionAlgorithms: ["A256GCM"],
538
- ...audience ? { audience } : {}
539
- });
540
- const { accessToken, refreshToken, accessExpiresAt, user } = payload;
541
- if (typeof accessToken !== "string" || typeof refreshToken !== "string" || !Number.isFinite(accessExpiresAt) || !user || typeof user !== "object" || typeof user.recordId !== "string" || typeof user.email !== "string") {
542
- return null;
543
- }
544
- return {
545
- accessToken,
546
- refreshToken,
547
- accessExpiresAt,
548
- user: {
549
- recordId: user.recordId,
550
- email: user.email
551
- }
552
- };
553
- } catch {
554
- return null;
555
- }
556
- }
557
- function isAccessExpired(payload, now = Date.now()) {
558
- return payload.accessExpiresAt <= now + ACCESS_EXPIRY_SKEW_MS;
559
- }
560
- function sessionCookieOptions() {
561
- return {
562
- httpOnly: true,
563
- secure: process.env.NODE_ENV !== "development",
564
- sameSite: "lax",
565
- path: "/",
566
- maxAge: SESSION_MAX_AGE_SECONDS
567
- };
568
- }
569
-
570
- // src/config.ts
571
- function assertAuthConfig(config) {
572
- const auth = config.auth;
573
- if (!auth || typeof auth.modelSlug !== "string" || !auth.modelSlug) {
574
- throw new Error("cmssy: config.auth.modelSlug is required for auth routes");
575
- }
576
- if (typeof auth.sessionSecret !== "string" || auth.sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
577
- throw new Error(
578
- `cmssy: config.auth.sessionSecret must be at least ${MIN_SESSION_SECRET_LENGTH} characters`
579
- );
580
- }
581
- return auth;
582
- }
583
630
  var LOGIN_MUTATION = `mutation SiteMemberLogin($input: SiteMemberLoginInput!) {
584
631
  siteMemberLogin(input: $input) {
585
632
  success message accessToken refreshToken accessTokenExpiresIn
@@ -1287,27 +1334,6 @@ function createCmssyCartRoute(config) {
1287
1334
  }
1288
1335
  };
1289
1336
  }
1290
- async function readValidSession(config) {
1291
- const auth = assertAuthConfig(config);
1292
- const jar = await cookies();
1293
- const raw = jar.get(CMSSY_SESSION_COOKIE)?.value;
1294
- if (!raw) return null;
1295
- const session = await openSession(
1296
- raw,
1297
- auth.sessionSecret,
1298
- config.workspaceSlug
1299
- );
1300
- if (!session || isAccessExpired(session)) return null;
1301
- return session;
1302
- }
1303
- async function getCmssyUser(config) {
1304
- const session = await readValidSession(config);
1305
- return session?.user ?? null;
1306
- }
1307
- async function getCmssyAccessToken(config) {
1308
- const session = await readValidSession(config);
1309
- return session?.accessToken ?? null;
1310
- }
1311
1337
  function isPrefetch(request2) {
1312
1338
  return request2.headers.get("next-router-prefetch") !== null || request2.headers.get("purpose") === "prefetch" || (request2.headers.get("sec-purpose") ?? "").includes("prefetch");
1313
1339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/next",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Next.js App Router bindings for cmssy headless sites (createCmssyPage + draft preview)",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -41,7 +41,7 @@
41
41
  "dist"
42
42
  ],
43
43
  "peerDependencies": {
44
- "@cmssy/react": "^0.5.4",
44
+ "@cmssy/react": "^0.5.5",
45
45
  "next": ">=15",
46
46
  "react": "^18.2.0 || ^19.0.0",
47
47
  "react-dom": "^18.2.0 || ^19.0.0"
@@ -54,7 +54,7 @@
54
54
  "tsup": "^8.3.0",
55
55
  "typescript": "^5.6.0",
56
56
  "vitest": "^2.1.0",
57
- "@cmssy/react": "0.5.4"
57
+ "@cmssy/react": "0.5.5"
58
58
  },
59
59
  "dependencies": {
60
60
  "jose": "^6.2.3"