@stackframe/stack-shared 2.7.14 → 2.7.17
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 +21 -0
- package/dist/helpers/password.js +1 -1
- package/dist/interface/clientInterface.d.ts +1 -1
- package/dist/interface/clientInterface.js +9 -6
- package/dist/interface/crud/contact-channels.js +5 -5
- package/dist/known-errors.js +9 -9
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.js +9 -0
- package/dist/utils/strings.d.ts +1 -1
- package/dist/utils/strings.js +31 -1
- package/dist/utils/strings.test.d.ts +1 -0
- package/dist/utils/strings.test.js +26 -0
- package/dist/utils/urls.d.ts +1 -0
- package/dist/utils/urls.js +7 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @stackframe/stack-shared
|
|
2
2
|
|
|
3
|
+
## 2.7.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Various changes
|
|
8
|
+
- @stackframe/stack-sc@2.7.17
|
|
9
|
+
|
|
10
|
+
## 2.7.16
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Various changes
|
|
15
|
+
- @stackframe/stack-sc@2.7.16
|
|
16
|
+
|
|
17
|
+
## 2.7.15
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Various changes
|
|
22
|
+
- @stackframe/stack-sc@2.7.15
|
|
23
|
+
|
|
3
24
|
## 2.7.14
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/dist/helpers/password.js
CHANGED
|
@@ -13,7 +13,7 @@ import { TeamPermissionsCrud } from './crud/team-permissions';
|
|
|
13
13
|
import { TeamsCrud } from './crud/teams';
|
|
14
14
|
export type ClientInterfaceOptions = {
|
|
15
15
|
clientVersion: string;
|
|
16
|
-
|
|
16
|
+
getBaseUrl: () => string;
|
|
17
17
|
projectId: string;
|
|
18
18
|
} & ({
|
|
19
19
|
publishableClientKey: string;
|
|
@@ -19,7 +19,7 @@ export class StackClientInterface {
|
|
|
19
19
|
return this.options.projectId;
|
|
20
20
|
}
|
|
21
21
|
getApiUrl() {
|
|
22
|
-
return this.options.
|
|
22
|
+
return this.options.getBaseUrl() + "/api/v1";
|
|
23
23
|
}
|
|
24
24
|
async runNetworkDiagnostics(session, requestType) {
|
|
25
25
|
const tryRequest = async (cb) => {
|
|
@@ -101,7 +101,7 @@ export class StackClientInterface {
|
|
|
101
101
|
throw new Error("Admin session token is currently not supported for fetching new access token. Did you try to log in on a StackApp initiated with the admin session?");
|
|
102
102
|
}
|
|
103
103
|
const as = {
|
|
104
|
-
issuer: this.options.
|
|
104
|
+
issuer: this.options.getBaseUrl(),
|
|
105
105
|
algorithm: 'oauth2',
|
|
106
106
|
token_endpoint: this.getApiUrl() + '/auth/oauth/token',
|
|
107
107
|
};
|
|
@@ -168,7 +168,10 @@ export class StackClientInterface {
|
|
|
168
168
|
let adminTokenObj = adminSession ? await adminSession.getOrFetchLikelyValidTokens(20000) : null;
|
|
169
169
|
// all requests should be dynamic to prevent Next.js caching
|
|
170
170
|
await cookies?.();
|
|
171
|
-
|
|
171
|
+
let url = this.getApiUrl() + path;
|
|
172
|
+
if (url.endsWith("/")) {
|
|
173
|
+
url = url.slice(0, -1);
|
|
174
|
+
}
|
|
172
175
|
const params = {
|
|
173
176
|
/**
|
|
174
177
|
* This fetch may be cross-origin, in which case we don't want to send cookies of the
|
|
@@ -277,10 +280,10 @@ export class StackClientInterface {
|
|
|
277
280
|
}
|
|
278
281
|
else {
|
|
279
282
|
const error = await res.text();
|
|
280
|
-
const errorObj = new StackAssertionError(`Failed to send request to ${url}: ${res.status} ${error}`, { request: params, res });
|
|
283
|
+
const errorObj = new StackAssertionError(`Failed to send request to ${url}: ${res.status} ${error}`, { request: params, res, path });
|
|
281
284
|
if (res.status === 508 && error.includes("INFINITE_LOOP_DETECTED")) {
|
|
282
285
|
// Some Vercel deployments seem to have an odd infinite loop bug. In that case, retry.
|
|
283
|
-
// See: https://github.com/stack-auth/stack/issues/319
|
|
286
|
+
// See: https://github.com/stack-auth/stack-auth/issues/319
|
|
284
287
|
return Result.error(errorObj);
|
|
285
288
|
}
|
|
286
289
|
// Do not retry, throw error instead of returning one
|
|
@@ -652,7 +655,7 @@ export class StackClientInterface {
|
|
|
652
655
|
throw new Error("Admin session token is currently not supported for OAuth");
|
|
653
656
|
}
|
|
654
657
|
const as = {
|
|
655
|
-
issuer: this.options.
|
|
658
|
+
issuer: this.options.getBaseUrl(),
|
|
656
659
|
algorithm: 'oauth2',
|
|
657
660
|
token_endpoint: this.getApiUrl() + '/auth/oauth/token',
|
|
658
661
|
};
|
|
@@ -39,27 +39,27 @@ export const contactChannelsCrud = createCrud({
|
|
|
39
39
|
docs: {
|
|
40
40
|
clientRead: {
|
|
41
41
|
summary: "Get a contact channel",
|
|
42
|
-
description: "",
|
|
42
|
+
description: "Retrieves a specific contact channel by the user ID and the contact channel ID.",
|
|
43
43
|
tags: ["Contact Channels"],
|
|
44
44
|
},
|
|
45
45
|
clientCreate: {
|
|
46
46
|
summary: "Create a contact channel",
|
|
47
|
-
description: "",
|
|
47
|
+
description: "Add a new contact channel for a user.",
|
|
48
48
|
tags: ["Contact Channels"],
|
|
49
49
|
},
|
|
50
50
|
clientUpdate: {
|
|
51
51
|
summary: "Update a contact channel",
|
|
52
|
-
description: "",
|
|
52
|
+
description: "Updates an existing contact channel. Only the values provided will be updated.",
|
|
53
53
|
tags: ["Contact Channels"],
|
|
54
54
|
},
|
|
55
55
|
clientDelete: {
|
|
56
56
|
summary: "Delete a contact channel",
|
|
57
|
-
description: "",
|
|
57
|
+
description: "Removes a contact channel for a given user.",
|
|
58
58
|
tags: ["Contact Channels"],
|
|
59
59
|
},
|
|
60
60
|
clientList: {
|
|
61
61
|
summary: "List contact channels",
|
|
62
|
-
description: "",
|
|
62
|
+
description: "Retrieves a list of all contact channels for a user.",
|
|
63
63
|
tags: ["Contact Channels"],
|
|
64
64
|
}
|
|
65
65
|
}
|
package/dist/known-errors.js
CHANGED
|
@@ -260,7 +260,7 @@ const ProviderRejected = createKnownErrorConstructor(RefreshTokenError, "PROVIDE
|
|
|
260
260
|
"The provider refused to refresh their token. This usually means that the provider used to authenticate the user no longer regards this session as valid, and the user must re-authenticate.",
|
|
261
261
|
], () => []);
|
|
262
262
|
const UserEmailAlreadyExists = createKnownErrorConstructor(KnownError, "USER_EMAIL_ALREADY_EXISTS", () => [
|
|
263
|
-
|
|
263
|
+
409,
|
|
264
264
|
"User email already exists.",
|
|
265
265
|
], () => []);
|
|
266
266
|
const CannotGetOwnUserWithoutUser = createKnownErrorConstructor(KnownError, "CANNOT_GET_OWN_USER_WITHOUT_USER", () => [
|
|
@@ -346,7 +346,7 @@ const VerificationCodeExpired = createKnownErrorConstructor(VerificationCodeErro
|
|
|
346
346
|
"The verification code has expired.",
|
|
347
347
|
], () => []);
|
|
348
348
|
const VerificationCodeAlreadyUsed = createKnownErrorConstructor(VerificationCodeError, "VERIFICATION_CODE_ALREADY_USED", () => [
|
|
349
|
-
|
|
349
|
+
409,
|
|
350
350
|
"The verification link has already been used.",
|
|
351
351
|
], () => []);
|
|
352
352
|
const VerificationCodeMaxAttemptsReached = createKnownErrorConstructor(VerificationCodeError, "VERIFICATION_CODE_MAX_ATTEMPTS_REACHED", () => [
|
|
@@ -358,7 +358,7 @@ const PasswordConfirmationMismatch = createKnownErrorConstructor(KnownError, "PA
|
|
|
358
358
|
"Passwords do not match.",
|
|
359
359
|
], () => []);
|
|
360
360
|
const EmailAlreadyVerified = createKnownErrorConstructor(KnownError, "EMAIL_ALREADY_VERIFIED", () => [
|
|
361
|
-
|
|
361
|
+
409,
|
|
362
362
|
"The e-mail is already verified.",
|
|
363
363
|
], () => []);
|
|
364
364
|
const EmailNotAssociatedWithUser = createKnownErrorConstructor(KnownError, "EMAIL_NOT_ASSOCIATED_WITH_USER", () => [
|
|
@@ -411,7 +411,7 @@ const TeamNotFound = createKnownErrorConstructor(KnownError, "TEAM_NOT_FOUND", (
|
|
|
411
411
|
},
|
|
412
412
|
], (json) => [json.team_id]);
|
|
413
413
|
const TeamAlreadyExists = createKnownErrorConstructor(KnownError, "TEAM_ALREADY_EXISTS", (teamId) => [
|
|
414
|
-
|
|
414
|
+
409,
|
|
415
415
|
`Team ${teamId} already exists.`,
|
|
416
416
|
{
|
|
417
417
|
team_id: teamId,
|
|
@@ -426,7 +426,7 @@ const TeamMembershipNotFound = createKnownErrorConstructor(KnownError, "TEAM_MEM
|
|
|
426
426
|
},
|
|
427
427
|
], (json) => [json.team_id, json.user_id]);
|
|
428
428
|
const EmailTemplateAlreadyExists = createKnownErrorConstructor(KnownError, "EMAIL_TEMPLATE_ALREADY_EXISTS", () => [
|
|
429
|
-
|
|
429
|
+
409,
|
|
430
430
|
"Email template already exists.",
|
|
431
431
|
], () => []);
|
|
432
432
|
const OAuthConnectionNotConnectedToUser = createKnownErrorConstructor(KnownError, "OAUTH_CONNECTION_NOT_CONNECTED_TO_USER", () => [
|
|
@@ -434,7 +434,7 @@ const OAuthConnectionNotConnectedToUser = createKnownErrorConstructor(KnownError
|
|
|
434
434
|
"The OAuth connection is not connected to any user.",
|
|
435
435
|
], () => []);
|
|
436
436
|
const OAuthConnectionAlreadyConnectedToAnotherUser = createKnownErrorConstructor(KnownError, "OAUTH_CONNECTION_ALREADY_CONNECTED_TO_ANOTHER_USER", () => [
|
|
437
|
-
|
|
437
|
+
409,
|
|
438
438
|
"The OAuth connection is already connected to another user.",
|
|
439
439
|
], () => []);
|
|
440
440
|
const OAuthConnectionDoesNotHaveRequiredScope = createKnownErrorConstructor(KnownError, "OAUTH_CONNECTION_DOES_NOT_HAVE_REQUIRED_SCOPE", () => [
|
|
@@ -461,7 +461,7 @@ const InvalidScope = createKnownErrorConstructor(KnownError, "INVALID_SCOPE", (s
|
|
|
461
461
|
`The scope "${scope}" is not a valid OAuth scope for Stack.`,
|
|
462
462
|
], (json) => [json.scope]);
|
|
463
463
|
const UserAlreadyConnectedToAnotherOAuthConnection = createKnownErrorConstructor(KnownError, "USER_ALREADY_CONNECTED_TO_ANOTHER_OAUTH_CONNECTION", () => [
|
|
464
|
-
|
|
464
|
+
409,
|
|
465
465
|
"The user is already connected to another OAuth account. Did you maybe selected the wrong account?",
|
|
466
466
|
], () => []);
|
|
467
467
|
const OuterOAuthTimeout = createKnownErrorConstructor(KnownError, "OUTER_OAUTH_TIMEOUT", () => [
|
|
@@ -488,7 +488,7 @@ const UserAuthenticationRequired = createKnownErrorConstructor(KnownError, "USER
|
|
|
488
488
|
"User authentication required for this endpoint.",
|
|
489
489
|
], () => []);
|
|
490
490
|
const TeamMembershipAlreadyExists = createKnownErrorConstructor(KnownError, "TEAM_MEMBERSHIP_ALREADY_EXISTS", () => [
|
|
491
|
-
|
|
491
|
+
409,
|
|
492
492
|
"Team membership already exists.",
|
|
493
493
|
], () => []);
|
|
494
494
|
const TeamPermissionRequired = createKnownErrorConstructor(KnownError, "TEAM_PERMISSION_REQUIRED", (teamId, userId, permissionId) => [
|
|
@@ -532,7 +532,7 @@ const OAuthProviderAccessDenied = createKnownErrorConstructor(KnownError, "OAUTH
|
|
|
532
532
|
"The OAuth provider denied access to the user.",
|
|
533
533
|
], () => []);
|
|
534
534
|
const ContactChannelAlreadyUsedForAuthBySomeoneElse = createKnownErrorConstructor(KnownError, "CONTACT_CHANNEL_ALREADY_USED_FOR_AUTH_BY_SOMEONE_ELSE", (type) => [
|
|
535
|
-
|
|
535
|
+
409,
|
|
536
536
|
`This ${type} is already used for authentication by another account.`,
|
|
537
537
|
{ type },
|
|
538
538
|
], (json) => [json.type]);
|
package/dist/utils/fs.d.ts
CHANGED
package/dist/utils/fs.js
CHANGED
|
@@ -20,3 +20,12 @@ export async function listRecursively(p, options = {}) {
|
|
|
20
20
|
}))).flat(),
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
|
+
export function writeFileSyncIfChanged(path, content) {
|
|
24
|
+
if (stackFs.existsSync(path)) {
|
|
25
|
+
const existingContent = stackFs.readFileSync(path, "utf-8");
|
|
26
|
+
if (existingContent === content) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
stackFs.writeFileSync(path, content);
|
|
31
|
+
}
|
package/dist/utils/strings.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare function trimLines(s: string): string;
|
|
|
40
40
|
*
|
|
41
41
|
* Useful for implementing your own template literal tags.
|
|
42
42
|
*/
|
|
43
|
-
export declare function templateIdentity(strings: TemplateStringsArray | readonly string[], ...values:
|
|
43
|
+
export declare function templateIdentity(strings: TemplateStringsArray | readonly string[], ...values: string[]): string;
|
|
44
44
|
export declare function deindent(code: string): string;
|
|
45
45
|
export declare function deindent(strings: TemplateStringsArray | readonly string[], ...values: any[]): string;
|
|
46
46
|
export declare function extractScopes(scope: string, removeDuplicates?: boolean): string[];
|
package/dist/utils/strings.js
CHANGED
|
@@ -61,21 +61,51 @@ export function trimEmptyLinesEnd(s) {
|
|
|
61
61
|
export function trimLines(s) {
|
|
62
62
|
return trimEmptyLinesEnd(trimEmptyLinesStart(s));
|
|
63
63
|
}
|
|
64
|
+
import.meta.vitest?.test("trimLines", ({ expect }) => {
|
|
65
|
+
expect(trimLines("")).toBe("");
|
|
66
|
+
expect(trimLines(" ")).toBe("");
|
|
67
|
+
expect(trimLines(" \n ")).toBe("");
|
|
68
|
+
expect(trimLines(" abc ")).toBe(" abc ");
|
|
69
|
+
expect(trimLines("\n \nLine1\nLine2\n \n")).toBe("Line1\nLine2");
|
|
70
|
+
expect(trimLines("Line1\n \nLine2")).toBe("Line1\n \nLine2");
|
|
71
|
+
expect(trimLines(" \n \n\t")).toBe("");
|
|
72
|
+
expect(trimLines(" Hello World")).toBe(" Hello World");
|
|
73
|
+
expect(trimLines("\n")).toBe("");
|
|
74
|
+
expect(trimLines("\t \n\t\tLine1 \n \nLine2\t\t\n\t ")).toBe("\t\tLine1 \n \nLine2\t\t");
|
|
75
|
+
});
|
|
64
76
|
/**
|
|
65
77
|
* A template literal tag that returns the same string as the template literal without a tag.
|
|
66
78
|
*
|
|
67
79
|
* Useful for implementing your own template literal tags.
|
|
68
80
|
*/
|
|
69
81
|
export function templateIdentity(strings, ...values) {
|
|
82
|
+
if (values.length !== strings.length - 1)
|
|
83
|
+
throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
|
|
70
84
|
return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), '');
|
|
71
85
|
}
|
|
86
|
+
import.meta.vitest?.test("templateIdentity", ({ expect }) => {
|
|
87
|
+
expect(templateIdentity `Hello World`).toBe("Hello World");
|
|
88
|
+
expect(templateIdentity `${"Hello"}`).toBe("Hello");
|
|
89
|
+
const greeting = "Hello";
|
|
90
|
+
const subject = "World";
|
|
91
|
+
expect(templateIdentity `${greeting}, ${subject}!`).toBe("Hello, World!");
|
|
92
|
+
expect(templateIdentity `${"A"}${"B"}${"C"}`).toBe("ABC");
|
|
93
|
+
expect(templateIdentity `Start${""}Middle${""}End`).toBe("StartMiddleEnd");
|
|
94
|
+
expect(templateIdentity ``).toBe("");
|
|
95
|
+
expect(templateIdentity `Line1
|
|
96
|
+
Line2`).toBe("Line1\nLine2");
|
|
97
|
+
expect(templateIdentity(["a ", " scientific ", "gun"], "certain", "rail")).toBe("a certain scientific railgun");
|
|
98
|
+
expect(templateIdentity(["only one part"])).toBe("only one part");
|
|
99
|
+
expect(() => templateIdentity(["a ", "b", "c"], "only one")).toThrow("Invalid number of values");
|
|
100
|
+
expect(() => templateIdentity(["a", "b"], "x", "y")).toThrow("Invalid number of values");
|
|
101
|
+
});
|
|
72
102
|
export function deindent(strings, ...values) {
|
|
73
103
|
if (typeof strings === "string")
|
|
74
104
|
return deindent([strings]);
|
|
75
105
|
if (strings.length === 0)
|
|
76
106
|
return "";
|
|
77
107
|
if (values.length !== strings.length - 1)
|
|
78
|
-
throw new
|
|
108
|
+
throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
|
|
79
109
|
const trimmedStrings = [...strings];
|
|
80
110
|
trimmedStrings[0] = trimEmptyLinesStart(trimmedStrings[0]);
|
|
81
111
|
trimmedStrings[trimmedStrings.length - 1] = trimEmptyLinesEnd(trimmedStrings[trimmedStrings.length - 1]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { templateIdentity } from "./strings";
|
|
3
|
+
describe("templateIdentity", () => {
|
|
4
|
+
it("should be equivalent to a regular template string", () => {
|
|
5
|
+
const adjective = "scientific";
|
|
6
|
+
const noun = "railgun";
|
|
7
|
+
expect(templateIdentity `a certain scientific railgun`).toBe("a certain scientific railgun");
|
|
8
|
+
expect(templateIdentity `a certain ${adjective} railgun`).toBe(`a certain scientific railgun`);
|
|
9
|
+
expect(templateIdentity `a certain ${adjective} ${noun}`).toBe(`a certain scientific railgun`);
|
|
10
|
+
expect(templateIdentity `${adjective}${noun}`).toBe(`scientificrailgun`);
|
|
11
|
+
});
|
|
12
|
+
it("should work with empty strings", () => {
|
|
13
|
+
expect(templateIdentity ``).toBe("");
|
|
14
|
+
expect(templateIdentity `${""}`).toBe("");
|
|
15
|
+
expect(templateIdentity `${""}${""}`).toBe("");
|
|
16
|
+
});
|
|
17
|
+
it("should work with normal arrays", () => {
|
|
18
|
+
expect(templateIdentity(["a ", " scientific ", "gun"], "certain", "rail")).toBe("a certain scientific railgun");
|
|
19
|
+
expect(templateIdentity(["a"])).toBe("a");
|
|
20
|
+
});
|
|
21
|
+
it("should throw an error with wrong number of value arguments", () => {
|
|
22
|
+
expect(() => templateIdentity([])).toThrow();
|
|
23
|
+
expect(() => templateIdentity(["a", "b"])).toThrow();
|
|
24
|
+
expect(() => templateIdentity(["a", "b", "c"], "a", "b", "c")).toThrow();
|
|
25
|
+
});
|
|
26
|
+
});
|
package/dist/utils/urls.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function createUrlIfValid(...args: ConstructorParameters<typeof URL>): URL | null;
|
|
2
2
|
export declare function isValidUrl(url: string): boolean;
|
|
3
|
+
export declare function isValidHostname(hostname: string): boolean;
|
|
3
4
|
export declare function isLocalhost(urlOrString: string | URL): boolean;
|
|
4
5
|
export declare function isRelative(url: string): boolean;
|
|
5
6
|
export declare function getRelativePart(url: URL): string;
|
package/dist/utils/urls.js
CHANGED
|
@@ -11,6 +11,12 @@ export function createUrlIfValid(...args) {
|
|
|
11
11
|
export function isValidUrl(url) {
|
|
12
12
|
return !!createUrlIfValid(url);
|
|
13
13
|
}
|
|
14
|
+
export function isValidHostname(hostname) {
|
|
15
|
+
const url = createUrlIfValid(`https://${hostname}`);
|
|
16
|
+
if (!url)
|
|
17
|
+
return false;
|
|
18
|
+
return url.hostname === hostname;
|
|
19
|
+
}
|
|
14
20
|
export function isLocalhost(urlOrString) {
|
|
15
21
|
const url = createUrlIfValid(urlOrString);
|
|
16
22
|
if (!url)
|
|
@@ -49,5 +55,5 @@ export function url(strings, ...values) {
|
|
|
49
55
|
* Any values passed are encoded.
|
|
50
56
|
*/
|
|
51
57
|
export function urlString(strings, ...values) {
|
|
52
|
-
return templateIdentity(strings, values.map(encodeURIComponent));
|
|
58
|
+
return templateIdentity(strings, ...values.map(encodeURIComponent));
|
|
53
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackframe/stack-shared",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.17",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"oauth4webapi": "^2.10.3",
|
|
53
53
|
"semver": "^7.6.3",
|
|
54
54
|
"uuid": "^9.0.1",
|
|
55
|
-
"@stackframe/stack-sc": "2.7.
|
|
55
|
+
"@stackframe/stack-sc": "2.7.17"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@sentry/nextjs": "^8.40.0",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"scripts": {
|
|
70
70
|
"build": "tsc",
|
|
71
71
|
"typecheck": "tsc --noEmit",
|
|
72
|
+
"test": "vitest run",
|
|
72
73
|
"clean": "rimraf dist && rimraf node_modules",
|
|
73
74
|
"dev": "tsc -w --preserveWatchOutput --declarationMap",
|
|
74
75
|
"lint": "eslint --ext .tsx,.ts ."
|