@stackframe/stack-shared 2.6.34 → 2.6.36
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.
- package/CHANGELOG.md +14 -0
- package/dist/helpers/production-mode.js +7 -2
- package/dist/interface/clientInterface.js +1 -1
- package/dist/known-errors.js +2 -2
- package/dist/schema-fields.d.ts +5 -4
- package/dist/schema-fields.js +6 -5
- package/dist/sessions.d.ts +5 -2
- package/dist/sessions.js +9 -2
- package/dist/utils/browser-compat.d.ts +6 -0
- package/dist/utils/browser-compat.js +17 -0
- package/dist/utils/errors.d.ts +5 -11
- package/dist/utils/errors.js +11 -10
- package/dist/utils/jwt.js +4 -0
- package/dist/utils/results.d.ts +1 -1
- package/dist/utils/results.js +3 -3
- package/dist/utils/sentry.d.ts +2 -0
- package/dist/utils/sentry.js +15 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @stackframe/stack-shared
|
|
2
2
|
|
|
3
|
+
## 2.6.36
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Various updates
|
|
8
|
+
- @stackframe/stack-sc@2.6.36
|
|
9
|
+
|
|
10
|
+
## 2.6.35
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Bugfixes
|
|
15
|
+
- @stackframe/stack-sc@2.6.35
|
|
16
|
+
|
|
3
17
|
## 2.6.34
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StackAssertionError } from "../utils/errors";
|
|
1
|
+
import { StackAssertionError, captureError } from "../utils/errors";
|
|
2
2
|
import { isLocalhost } from "../utils/urls";
|
|
3
3
|
export function getProductionModeErrors(project) {
|
|
4
4
|
const errors = [];
|
|
@@ -15,10 +15,15 @@ export function getProductionModeErrors(project) {
|
|
|
15
15
|
url = new URL(domain);
|
|
16
16
|
}
|
|
17
17
|
catch (e) {
|
|
18
|
-
|
|
18
|
+
captureError("production-mode-domain-not-valid", new StackAssertionError("Domain was somehow not a valid URL; we should've caught this when setting the domain in the first place", {
|
|
19
19
|
domain,
|
|
20
20
|
projectId: project
|
|
21
|
+
}));
|
|
22
|
+
errors.push({
|
|
23
|
+
message: "Trusted domain is not a valid URL: " + domain,
|
|
24
|
+
relativeFixUrl: domainsFixUrl,
|
|
21
25
|
});
|
|
26
|
+
continue;
|
|
22
27
|
}
|
|
23
28
|
if (isLocalhost(url)) {
|
|
24
29
|
errors.push({
|
package/dist/known-errors.js
CHANGED
|
@@ -149,7 +149,7 @@ const AccessTypeWithoutProjectId = createKnownErrorConstructor(InvalidProjectAut
|
|
|
149
149
|
deindent `
|
|
150
150
|
The x-stack-access-type header was '${accessType}', but the x-stack-project-id header was not provided.
|
|
151
151
|
|
|
152
|
-
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/
|
|
152
|
+
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
|
|
153
153
|
`,
|
|
154
154
|
{
|
|
155
155
|
request_type: accessType,
|
|
@@ -160,7 +160,7 @@ const AccessTypeRequired = createKnownErrorConstructor(InvalidProjectAuthenticat
|
|
|
160
160
|
deindent `
|
|
161
161
|
You must specify an access level for this Stack project. Make sure project API keys are provided (eg. x-stack-publishable-client-key) and you set the x-stack-access-type header to 'client', 'server', or 'admin'.
|
|
162
162
|
|
|
163
|
-
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/
|
|
163
|
+
For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
|
|
164
164
|
`,
|
|
165
165
|
], () => []);
|
|
166
166
|
const InsufficientAccessType = createKnownErrorConstructor(InvalidProjectAuthentication, "INSUFFICIENT_ACCESS_TYPE", (actualAccessType, allowedAccessTypes) => [
|
package/dist/schema-fields.d.ts
CHANGED
|
@@ -33,16 +33,17 @@ export declare const jsonStringOrEmptySchema: yup.StringSchema<string | undefine
|
|
|
33
33
|
export declare const base64Schema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
34
34
|
export declare const passwordSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
35
35
|
/**
|
|
36
|
-
* A stricter email schema that does some additional checks for UX input.
|
|
36
|
+
* A stricter email schema that does some additional checks for UX input. (Some emails are allowed by the spec, for
|
|
37
|
+
* example `test@localhost` or `abc@gmail`, but almost certainly a user input error.)
|
|
37
38
|
*
|
|
38
39
|
* Note that some users in the DB have an email that doesn't match this regex, so most of the time you should use
|
|
39
40
|
* `emailSchema` instead until we do the DB migration.
|
|
40
41
|
*/
|
|
41
42
|
export declare const strictEmailSchema: (message: string | undefined) => yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
42
43
|
export declare const emailSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
43
|
-
export declare const clientOrHigherAuthTypeSchema: yup.StringSchema<"client" | "server" | "admin"
|
|
44
|
-
export declare const serverOrHigherAuthTypeSchema: yup.StringSchema<"server" | "admin"
|
|
45
|
-
export declare const adminAuthTypeSchema: yup.StringSchema<"admin"
|
|
44
|
+
export declare const clientOrHigherAuthTypeSchema: yup.StringSchema<"client" | "server" | "admin", yup.AnyObject, undefined, "">;
|
|
45
|
+
export declare const serverOrHigherAuthTypeSchema: yup.StringSchema<"server" | "admin", yup.AnyObject, undefined, "">;
|
|
46
|
+
export declare const adminAuthTypeSchema: yup.StringSchema<"admin", yup.AnyObject, undefined, "">;
|
|
46
47
|
export declare const projectIdSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
47
48
|
export declare const projectDisplayNameSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
48
49
|
export declare const projectDescriptionSchema: yup.StringSchema<string | null | undefined, yup.AnyObject, undefined, "">;
|
package/dist/schema-fields.js
CHANGED
|
@@ -195,19 +195,20 @@ export const base64Schema = yupString().test("is-base64", (params) => `${params.
|
|
|
195
195
|
});
|
|
196
196
|
export const passwordSchema = yupString().max(70);
|
|
197
197
|
/**
|
|
198
|
-
* A stricter email schema that does some additional checks for UX input.
|
|
198
|
+
* A stricter email schema that does some additional checks for UX input. (Some emails are allowed by the spec, for
|
|
199
|
+
* example `test@localhost` or `abc@gmail`, but almost certainly a user input error.)
|
|
199
200
|
*
|
|
200
201
|
* Note that some users in the DB have an email that doesn't match this regex, so most of the time you should use
|
|
201
202
|
* `emailSchema` instead until we do the DB migration.
|
|
202
203
|
*/
|
|
203
204
|
// eslint-disable-next-line no-restricted-syntax
|
|
204
|
-
export const strictEmailSchema = (message) => yupString().email(message).matches(
|
|
205
|
+
export const strictEmailSchema = (message) => yupString().email(message).matches(/^.*@.*\.[^.][^.]+$/, message);
|
|
205
206
|
// eslint-disable-next-line no-restricted-syntax
|
|
206
207
|
export const emailSchema = yupString().email();
|
|
207
208
|
// Request auth
|
|
208
|
-
export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']);
|
|
209
|
-
export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']);
|
|
210
|
-
export const adminAuthTypeSchema = yupString().oneOf(['admin']);
|
|
209
|
+
export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']).defined();
|
|
210
|
+
export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']).defined();
|
|
211
|
+
export const adminAuthTypeSchema = yupString().oneOf(['admin']).defined();
|
|
211
212
|
// Projects
|
|
212
213
|
export const projectIdSchema = yupString().test((v) => v === undefined || v === "internal" || isUuid(v)).meta({ openapiField: { description: _idDescription('project'), exampleValue: 'e0b52f4d-dece-408c-af49-d23061bb0f8d' } });
|
|
213
214
|
export const projectDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('project'), exampleValue: 'MyMusic' } });
|
package/dist/sessions.d.ts
CHANGED
|
@@ -27,9 +27,12 @@ export declare class InternalSession {
|
|
|
27
27
|
private _accessToken;
|
|
28
28
|
private readonly _refreshToken;
|
|
29
29
|
/**
|
|
30
|
-
* Whether the session as a whole is known to be invalid. Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
|
|
30
|
+
* Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
|
|
31
31
|
*
|
|
32
|
-
*
|
|
32
|
+
* It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is
|
|
33
|
+
* still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to
|
|
34
|
+
* be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed
|
|
35
|
+
* in an access token, eg. in a server-side request handler).
|
|
33
36
|
*/
|
|
34
37
|
private _knownToBeInvalid;
|
|
35
38
|
private _refreshPromise;
|
package/dist/sessions.js
CHANGED
|
@@ -25,14 +25,21 @@ export class InternalSession {
|
|
|
25
25
|
constructor(_options) {
|
|
26
26
|
this._options = _options;
|
|
27
27
|
/**
|
|
28
|
-
* Whether the session as a whole is known to be invalid. Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
|
|
28
|
+
* Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
|
|
29
29
|
*
|
|
30
|
-
*
|
|
30
|
+
* It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is
|
|
31
|
+
* still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to
|
|
32
|
+
* be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed
|
|
33
|
+
* in an access token, eg. in a server-side request handler).
|
|
31
34
|
*/
|
|
32
35
|
this._knownToBeInvalid = new Store(false);
|
|
33
36
|
this._refreshPromise = null;
|
|
34
37
|
this._accessToken = new Store(_options.accessToken ? new AccessToken(_options.accessToken) : null);
|
|
35
38
|
this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;
|
|
39
|
+
if (_options.accessToken === null && _options.refreshToken === null) {
|
|
40
|
+
// this session is already invalid
|
|
41
|
+
this._knownToBeInvalid.set(true);
|
|
42
|
+
}
|
|
36
43
|
this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });
|
|
37
44
|
}
|
|
38
45
|
static calculateSessionKey(ofTokens) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function getBrowserCompatibilityReport() {
|
|
2
|
+
const test = (snippet) => {
|
|
3
|
+
try {
|
|
4
|
+
(0, eval)(snippet);
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
return `FAILED: ${e}`;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
optionalChaining: test("({})?.b?.c"),
|
|
13
|
+
nullishCoalescing: test("0 ?? 1"),
|
|
14
|
+
weakRef: test("new WeakRef({})"),
|
|
15
|
+
cryptoUuid: test("crypto.randomUUID()"),
|
|
16
|
+
};
|
|
17
|
+
}
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ export declare function throwErr(errorMessage: string, extraData?: any): never;
|
|
|
3
3
|
export declare function throwErr(error: Error): never;
|
|
4
4
|
export declare function throwErr(...args: StatusErrorConstructorParameters): never;
|
|
5
5
|
/**
|
|
6
|
-
* Concatenates the stacktraces of the given errors onto the first.
|
|
6
|
+
* Concatenates the (original) stacktraces of the given errors onto the first.
|
|
7
7
|
*
|
|
8
8
|
* Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
|
|
9
9
|
* enough to keep track of the call stack in async function calls when you invoke `.then` within the same async tick,
|
|
10
|
-
* but if you don't,
|
|
10
|
+
* but if you don't, the stacktrace will be lost.
|
|
11
11
|
*
|
|
12
12
|
* Here's an example of the unwanted behavior:
|
|
13
13
|
*
|
|
@@ -24,16 +24,10 @@ export declare function throwErr(...args: StatusErrorConstructorParameters): nev
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
export declare function concatStacktraces(first: Error, ...errors: Error[]): void;
|
|
27
|
-
export declare class StackAssertionError extends Error
|
|
28
|
-
readonly extraData?: Record<string, any> | undefined;
|
|
29
|
-
constructor(message: string, extraData?: Record<string, any> | undefined
|
|
30
|
-
customCaptureExtraArgs: {
|
|
31
|
-
cause?: {} | undefined;
|
|
32
|
-
}[];
|
|
27
|
+
export declare class StackAssertionError extends Error {
|
|
28
|
+
readonly extraData?: (Record<string, any> & ErrorOptions) | undefined;
|
|
29
|
+
constructor(message: string, extraData?: (Record<string, any> & ErrorOptions) | undefined);
|
|
33
30
|
}
|
|
34
|
-
export type ErrorWithCustomCapture = {
|
|
35
|
-
customCaptureExtraArgs: any[];
|
|
36
|
-
};
|
|
37
31
|
export declare function registerErrorSink(sink: (location: string, error: unknown) => void): void;
|
|
38
32
|
export declare function captureError(location: string, error: unknown): void;
|
|
39
33
|
type Status = {
|
package/dist/utils/errors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { globalVar } from "./globals";
|
|
2
|
+
import { pick } from "./objects";
|
|
2
3
|
export function throwErr(...args) {
|
|
3
4
|
if (typeof args[0] === "string") {
|
|
4
5
|
throw new StackAssertionError(args[0], args[1]);
|
|
@@ -17,11 +18,11 @@ function removeStacktraceNameLine(stack) {
|
|
|
17
18
|
return stack.split("\n").slice(addsNameLine ? 1 : 0).join("\n");
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
* Concatenates the stacktraces of the given errors onto the first.
|
|
21
|
+
* Concatenates the (original) stacktraces of the given errors onto the first.
|
|
21
22
|
*
|
|
22
23
|
* Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
|
|
23
24
|
* enough to keep track of the call stack in async function calls when you invoke `.then` within the same async tick,
|
|
24
|
-
* but if you don't,
|
|
25
|
+
* but if you don't, the stacktrace will be lost.
|
|
25
26
|
*
|
|
26
27
|
* Here's an example of the unwanted behavior:
|
|
27
28
|
*
|
|
@@ -50,16 +51,16 @@ export function concatStacktraces(first, ...errors) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
export class StackAssertionError extends Error {
|
|
53
|
-
constructor(message, extraData
|
|
54
|
+
constructor(message, extraData) {
|
|
54
55
|
const disclaimer = `\n\nThis is likely an error in Stack. Please make sure you are running the newest version and report it.`;
|
|
55
|
-
super(`${message}${message.endsWith(disclaimer) ? "" : disclaimer}`,
|
|
56
|
+
super(`${message}${message.endsWith(disclaimer) ? "" : disclaimer}`, pick(extraData ?? {}, ["cause"]));
|
|
56
57
|
this.extraData = extraData;
|
|
57
|
-
this
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
...this.cause ? { cause: this.cause } : {},
|
|
58
|
+
Object.defineProperty(this, "customCaptureExtraArgs", {
|
|
59
|
+
get() {
|
|
60
|
+
return [this.extraData];
|
|
61
61
|
},
|
|
62
|
-
|
|
62
|
+
enumerable: false,
|
|
63
|
+
});
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
StackAssertionError.prototype.name = "StackAssertionError";
|
|
@@ -92,7 +93,7 @@ export class StatusError extends Error {
|
|
|
92
93
|
this.name = "StatusError";
|
|
93
94
|
this.statusCode = status;
|
|
94
95
|
if (!message) {
|
|
95
|
-
throw new StackAssertionError("StatusError always requires a message unless a Status object is passed", {
|
|
96
|
+
throw new StackAssertionError("StatusError always requires a message unless a Status object is passed", { cause: this });
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
isClientError() {
|
package/dist/utils/jwt.js
CHANGED
|
@@ -3,6 +3,7 @@ import elliptic from "elliptic";
|
|
|
3
3
|
import * as jose from "jose";
|
|
4
4
|
import { JOSEError } from "jose/errors";
|
|
5
5
|
import { encodeBase64Url } from "./bytes";
|
|
6
|
+
import { StackAssertionError } from "./errors";
|
|
6
7
|
import { globalVar } from "./globals";
|
|
7
8
|
import { pick } from "./objects";
|
|
8
9
|
const STACK_SERVER_SECRET = process.env.STACK_SERVER_SECRET ?? "";
|
|
@@ -74,6 +75,9 @@ export async function getPublicJwkSet(secretOrPrivateJwk) {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
export function getPerAudienceSecret(options) {
|
|
78
|
+
if (options.audience === "kid") {
|
|
79
|
+
throw new StackAssertionError("You cannot use the 'kid' audience for a per-audience secret, see comment below in jwt.tsx");
|
|
80
|
+
}
|
|
77
81
|
return jose.base64url.encode(crypto
|
|
78
82
|
.createHash('sha256')
|
|
79
83
|
// TODO we should prefix a string like "stack-audience-secret" before we hash so you can't use `getKid(...)` to get the secret for eg. the "kid" audience if the same secret value is used
|
package/dist/utils/results.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ declare class RetryError extends AggregateError {
|
|
|
69
69
|
constructor(errors: unknown[]);
|
|
70
70
|
get retries(): number;
|
|
71
71
|
}
|
|
72
|
-
declare function retry<T>(fn: () => Result<T> | Promise<Result<T>>, retries: number, { exponentialDelayBase }
|
|
72
|
+
declare function retry<T>(fn: (attempt: number) => Result<T> | Promise<Result<T>>, retries: number, { exponentialDelayBase }?: {
|
|
73
73
|
exponentialDelayBase?: number | undefined;
|
|
74
74
|
}): Promise<Result<T, RetryError>>;
|
|
75
75
|
export {};
|
package/dist/utils/results.js
CHANGED
|
@@ -116,17 +116,17 @@ class RetryError extends AggregateError {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
RetryError.prototype.name = "RetryError";
|
|
119
|
-
async function retry(fn, retries, { exponentialDelayBase =
|
|
119
|
+
async function retry(fn, retries, { exponentialDelayBase = 1000 } = {}) {
|
|
120
120
|
const errors = [];
|
|
121
121
|
for (let i = 0; i < retries; i++) {
|
|
122
|
-
const res = await fn();
|
|
122
|
+
const res = await fn(i);
|
|
123
123
|
if (res.status === "ok") {
|
|
124
124
|
return Result.ok(res.data);
|
|
125
125
|
}
|
|
126
126
|
else {
|
|
127
127
|
errors.push(res.error);
|
|
128
128
|
if (i < retries - 1) {
|
|
129
|
-
await wait(Math.random() * exponentialDelayBase * 2 ** i);
|
|
129
|
+
await wait((Math.random() + 0.5) * exponentialDelayBase * (2 ** i));
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const sentryBaseConfig = {
|
|
2
|
+
ignoreErrors: [
|
|
3
|
+
// React throws these errors when used with some browser extensions (eg. Google Translate)
|
|
4
|
+
"NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.",
|
|
5
|
+
"NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.",
|
|
6
|
+
],
|
|
7
|
+
normalizeDepth: 5,
|
|
8
|
+
maxValueLength: 5000,
|
|
9
|
+
// Adjust this value in production, or use tracesSampler for greater control
|
|
10
|
+
tracesSampleRate: 1.0,
|
|
11
|
+
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
|
12
|
+
debug: false,
|
|
13
|
+
replaysOnErrorSampleRate: 1.0,
|
|
14
|
+
replaysSessionSampleRate: 1.0,
|
|
15
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackframe/stack-shared",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.36",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"oauth4webapi": "^2.10.3",
|
|
51
51
|
"semver": "^7.6.3",
|
|
52
52
|
"uuid": "^9.0.1",
|
|
53
|
-
"@stackframe/stack-sc": "2.6.
|
|
53
|
+
"@stackframe/stack-sc": "2.6.36"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@simplewebauthn/types": "^11.0.0",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"rimraf": "^5.0.5",
|
|
62
62
|
"react": "^18.2",
|
|
63
63
|
"react-dom": "^18.2",
|
|
64
|
-
"next": "^14.1.0"
|
|
64
|
+
"next": "^14.1.0",
|
|
65
|
+
"@sentry/nextjs": "^8.40.0"
|
|
65
66
|
},
|
|
66
67
|
"scripts": {
|
|
67
68
|
"build": "tsc",
|