@stackframe/stack-shared 2.4.21 → 2.4.22

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @stackframe/stack-shared
2
2
 
3
+ ## 2.4.22
4
+
5
+ ### Patch Changes
6
+
7
+ - OAuth scopes
8
+ - Updated dependencies
9
+ - @stackframe/stack-sc@2.4.22
10
+
3
11
  ## 2.4.21
4
12
 
5
13
  ### Patch Changes
@@ -15,7 +15,6 @@ export type OAuthProviderUpdateOptions = {
15
15
  type: StandardProvider;
16
16
  clientId: string;
17
17
  clientSecret: string;
18
- tenantId?: string;
19
18
  });
20
19
  export type ProjectUpdateOptions = {
21
20
  displayName?: string;
@@ -78,7 +78,6 @@ export type OAuthProviderConfigJson = {
78
78
  type: StandardProvider;
79
79
  clientId: string;
80
80
  clientSecret: string;
81
- tenantId?: string;
82
81
  });
83
82
  export type EmailConfigJson = ({
84
83
  type: "standard";
@@ -173,9 +172,29 @@ export declare class StackClientInterface {
173
172
  accessToken: string;
174
173
  refreshToken: string;
175
174
  }>;
176
- getOAuthUrl(provider: string, redirectUrl: string, codeChallenge: string, state: string): Promise<string>;
177
- callOAuthCallback(oauthParams: URLSearchParams, redirectUri: string, codeVerifier: string, state: string): Promise<{
175
+ getOAuthUrl(options: {
176
+ provider: string;
177
+ redirectUrl: string;
178
+ errorRedirectUrl: string;
179
+ afterCallbackRedirectUrl?: string;
180
+ codeChallenge: string;
181
+ state: string;
182
+ type: "authenticate" | "link";
183
+ providerScope?: string;
184
+ } & ({
185
+ type: "authenticate";
186
+ } | {
187
+ type: "link";
188
+ session: InternalSession;
189
+ })): Promise<string>;
190
+ callOAuthCallback(options: {
191
+ oauthParams: URLSearchParams;
192
+ redirectUri: string;
193
+ codeVerifier: string;
194
+ state: string;
195
+ }): Promise<{
178
196
  newUser: boolean;
197
+ afterCallbackRedirectUrl?: string;
179
198
  accessToken: string;
180
199
  refreshToken: string;
181
200
  }>;
@@ -193,6 +212,9 @@ export declare class StackClientInterface {
193
212
  createProject(project: ProjectUpdateOptions & {
194
213
  displayName: string;
195
214
  }, session: InternalSession): Promise<ProjectJson>;
215
+ getAccessToken(provider: string, scope: string, session: InternalSession): Promise<{
216
+ accessToken: string;
217
+ }>;
196
218
  }
197
219
  export declare function getProductionModeErrors(project: ProjectJson): ProductionModeError[];
198
220
  export {};
@@ -66,11 +66,6 @@ export class StackClientInterface {
66
66
  const body = await response.data.text();
67
67
  throw new Error(`Failed to send refresh token request: ${response.status} ${body}`);
68
68
  }
69
- let challenges;
70
- if ((challenges = oauth.parseWwwAuthenticateChallenges(response.data))) {
71
- // TODO Handle WWW-Authenticate Challenges as needed
72
- throw new StackAssertionError("OAuth WWW-Authenticate challenge not implemented", { challenges });
73
- }
74
69
  const result = await oauth.processRefreshTokenResponse(as, client, response.data);
75
70
  if (oauth.isOAuth2Error(result)) {
76
71
  // TODO Handle OAuth 2.0 response body error
@@ -128,28 +123,28 @@ export class StackClientInterface {
128
123
  * However, Cloudflare Workers don't actually support `credentials`, so we only set it
129
124
  * if Cloudflare-exclusive globals are not detected. https://github.com/cloudflare/workers-sdk/issues/2514
130
125
  */
131
- ..."WebSocketPair" in globalVar ? {} : {
126
+ ...("WebSocketPair" in globalVar ? {} : {
132
127
  credentials: "omit",
133
- },
128
+ }),
134
129
  ...options,
135
130
  headers: {
136
131
  "X-Stack-Override-Error-Status": "true",
137
132
  "X-Stack-Project-Id": this.projectId,
138
133
  "X-Stack-Request-Type": requestType,
139
134
  "X-Stack-Client-Version": this.options.clientVersion,
140
- ...tokenObj ? {
135
+ ...(tokenObj ? {
141
136
  "Authorization": "StackSession " + tokenObj.accessToken.token,
142
137
  "X-Stack-Access-Token": tokenObj.accessToken.token,
143
- } : {},
144
- ...tokenObj?.refreshToken ? {
138
+ } : {}),
139
+ ...(tokenObj?.refreshToken ? {
145
140
  "X-Stack-Refresh-Token": tokenObj.refreshToken.token,
146
- } : {},
147
- ...'publishableClientKey' in this.options ? {
141
+ } : {}),
142
+ ...('publishableClientKey' in this.options ? {
148
143
  "X-Stack-Publishable-Client-Key": this.options.publishableClientKey,
149
- } : {},
150
- ...adminTokenObj ? {
144
+ } : {}),
145
+ ...(adminTokenObj ? {
151
146
  "X-Stack-Admin-Access-Token": adminTokenObj.accessToken.token,
152
- } : {},
147
+ } : {}),
153
148
  /**
154
149
  * Next.js until v15 would cache fetch requests by default, and forcefully disabling it was nearly impossible.
155
150
  *
@@ -164,9 +159,9 @@ export class StackClientInterface {
164
159
  /**
165
160
  * Cloudflare Workers does not support cache, so don't pass it there
166
161
  */
167
- ..."WebSocketPair" in globalVar ? {} : {
162
+ ...("WebSocketPair" in globalVar ? {} : {
168
163
  cache: "no-store",
169
- },
164
+ }),
170
165
  };
171
166
  let rawRes;
172
167
  try {
@@ -386,8 +381,8 @@ export class StackClientInterface {
386
381
  newUser: result.newUser,
387
382
  };
388
383
  }
389
- async getOAuthUrl(provider, redirectUrl, codeChallenge, state) {
390
- const updatedRedirectUrl = new URL(redirectUrl);
384
+ async getOAuthUrl(options) {
385
+ const updatedRedirectUrl = new URL(options.redirectUrl);
391
386
  for (const key of ["code", "state"]) {
392
387
  if (updatedRedirectUrl.searchParams.has(key)) {
393
388
  console.warn("Redirect URL already contains " + key + " parameter, removing it as it will be overwritten by the OAuth callback");
@@ -398,19 +393,31 @@ export class StackClientInterface {
398
393
  // TODO fix
399
394
  throw new Error("Admin session token is currently not supported for OAuth");
400
395
  }
401
- const url = new URL(this.getApiUrl() + "/auth/authorize/" + provider.toLowerCase());
396
+ const url = new URL(this.getApiUrl() + "/auth/authorize/" + options.provider.toLowerCase());
402
397
  url.searchParams.set("client_id", this.projectId);
403
398
  url.searchParams.set("client_secret", this.options.publishableClientKey);
404
399
  url.searchParams.set("redirect_uri", updatedRedirectUrl.toString());
405
400
  url.searchParams.set("scope", "openid");
406
- url.searchParams.set("state", state);
401
+ url.searchParams.set("state", options.state);
407
402
  url.searchParams.set("grant_type", "authorization_code");
408
- url.searchParams.set("code_challenge", codeChallenge);
403
+ url.searchParams.set("code_challenge", options.codeChallenge);
409
404
  url.searchParams.set("code_challenge_method", "S256");
410
405
  url.searchParams.set("response_type", "code");
406
+ url.searchParams.set("type", options.type);
407
+ url.searchParams.set("errorRedirectUrl", options.errorRedirectUrl);
408
+ if (options.afterCallbackRedirectUrl) {
409
+ url.searchParams.set("afterCallbackRedirectUrl", options.afterCallbackRedirectUrl);
410
+ }
411
+ if (options.type === "link") {
412
+ const tokens = await options.session.getPotentiallyExpiredTokens();
413
+ url.searchParams.set("token", tokens?.accessToken.token || "");
414
+ if (options.providerScope) {
415
+ url.searchParams.set("providerScope", options.providerScope);
416
+ }
417
+ }
411
418
  return url.toString();
412
419
  }
413
- async callOAuthCallback(oauthParams, redirectUri, codeVerifier, state) {
420
+ async callOAuthCallback(options) {
414
421
  if (!('publishableClientKey' in this.options)) {
415
422
  // TODO fix
416
423
  throw new Error("Admin session token is currently not supported for OAuth");
@@ -425,16 +432,11 @@ export class StackClientInterface {
425
432
  client_secret: this.options.publishableClientKey,
426
433
  token_endpoint_auth_method: 'client_secret_basic',
427
434
  };
428
- const params = oauth.validateAuthResponse(as, client, oauthParams, state);
435
+ const params = oauth.validateAuthResponse(as, client, options.oauthParams, options.state);
429
436
  if (oauth.isOAuth2Error(params)) {
430
437
  throw new StackAssertionError("Error validating outer OAuth response", { params }); // Handle OAuth 2.0 redirect error
431
438
  }
432
- const response = await oauth.authorizationCodeGrantRequest(as, client, params, redirectUri, codeVerifier);
433
- let challenges;
434
- if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
435
- // TODO Handle WWW-Authenticate Challenges as needed
436
- throw new StackAssertionError("Outer OAuth WWW-Authenticate challenge not implemented", { challenges });
437
- }
439
+ const response = await oauth.authorizationCodeGrantRequest(as, client, params, options.redirectUri, options.codeVerifier);
438
440
  const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response);
439
441
  if (oauth.isOAuth2Error(result)) {
440
442
  // TODO Handle OAuth 2.0 response body error
@@ -442,6 +444,7 @@ export class StackClientInterface {
442
444
  }
443
445
  return {
444
446
  newUser: result.newUser,
447
+ afterCallbackRedirectUrl: result.afterCallbackRedirectUrl,
445
448
  accessToken: result.access_token,
446
449
  refreshToken: result.refresh_token ?? throwErr("Refresh token not found in outer OAuth response"),
447
450
  };
@@ -523,6 +526,19 @@ export class StackClientInterface {
523
526
  const json = await fetchResponse.json();
524
527
  return json;
525
528
  }
529
+ async getAccessToken(provider, scope, session) {
530
+ const response = await this.sendClientRequest(`/auth/access-token/${provider}`, {
531
+ method: "POST",
532
+ headers: {
533
+ "content-type": "application/json",
534
+ },
535
+ body: JSON.stringify({ scope }),
536
+ }, session);
537
+ const json = await response.json();
538
+ return {
539
+ accessToken: json.accessToken,
540
+ };
541
+ }
526
542
  }
527
543
  export function getProductionModeErrors(project) {
528
544
  const errors = [];
@@ -0,0 +1,61 @@
1
+ import { CrudTypeOf } from "../../crud";
2
+ import * as yup from "yup";
3
+ export declare const accessTokenReadSchema: yup.ObjectSchema<{
4
+ accessToken: string;
5
+ }, yup.AnyObject, {
6
+ accessToken: undefined;
7
+ }, "">;
8
+ export declare const accessTokenCreateSchema: yup.ObjectSchema<{
9
+ scope: string | undefined;
10
+ }, yup.AnyObject, {
11
+ scope: undefined;
12
+ }, "">;
13
+ export declare const accessTokenCrud: {
14
+ client: {
15
+ createSchema: yup.ObjectSchema<{
16
+ scope: string | undefined;
17
+ }, yup.AnyObject, {
18
+ scope: undefined;
19
+ }, "">;
20
+ readSchema: yup.ObjectSchema<{
21
+ accessToken: string;
22
+ }, yup.AnyObject, {
23
+ accessToken: undefined;
24
+ }, "">;
25
+ updateSchema: undefined;
26
+ deleteSchema: undefined;
27
+ };
28
+ server: {
29
+ createSchema: yup.ObjectSchema<{
30
+ scope: string | undefined;
31
+ }, yup.AnyObject, {
32
+ scope: undefined;
33
+ }, "">;
34
+ readSchema: yup.ObjectSchema<{
35
+ accessToken: string;
36
+ }, yup.AnyObject, {
37
+ accessToken: undefined;
38
+ }, "">;
39
+ updateSchema: undefined;
40
+ deleteSchema: undefined;
41
+ };
42
+ admin: {
43
+ createSchema: yup.ObjectSchema<{
44
+ scope: string | undefined;
45
+ }, yup.AnyObject, {
46
+ scope: undefined;
47
+ }, "">;
48
+ readSchema: yup.ObjectSchema<{
49
+ accessToken: string;
50
+ }, yup.AnyObject, {
51
+ accessToken: undefined;
52
+ }, "">;
53
+ updateSchema: undefined;
54
+ deleteSchema: undefined;
55
+ };
56
+ hasCreate: boolean;
57
+ hasRead: boolean;
58
+ hasUpdate: boolean;
59
+ hasDelete: boolean;
60
+ };
61
+ export type AccessTokenCrud = CrudTypeOf<typeof accessTokenCrud>;
@@ -0,0 +1,12 @@
1
+ import { createCrud } from "../../crud";
2
+ import * as yup from "yup";
3
+ export const accessTokenReadSchema = yup.object({
4
+ accessToken: yup.string().required(),
5
+ }).required();
6
+ export const accessTokenCreateSchema = yup.object({
7
+ scope: yup.string().optional(),
8
+ }).required();
9
+ export const accessTokenCrud = createCrud({
10
+ clientReadSchema: accessTokenReadSchema,
11
+ clientCreateSchema: accessTokenCreateSchema,
12
+ });
@@ -236,11 +236,32 @@ export declare const KnownErrors: {
236
236
  PermissionScopeMismatch: KnownErrorConstructor<KnownError & KnownErrorBrand<"PERMISSION_SCOPE_MISMATCH">, [permissionId: string, permissionScope: PermissionDefinitionScopeJson, testScope: PermissionDefinitionScopeJson]> & {
237
237
  errorCode: "PERMISSION_SCOPE_MISMATCH";
238
238
  };
239
+ UserNotInTeam: KnownErrorConstructor<KnownError & KnownErrorBrand<"USER_NOT_IN_TEAM">, [userId: string, teamId: string]> & {
240
+ errorCode: "USER_NOT_IN_TEAM";
241
+ };
239
242
  TeamNotFound: KnownErrorConstructor<KnownError & KnownErrorBrand<"TEAM_NOT_FOUND">, [teamId: string]> & {
240
243
  errorCode: "TEAM_NOT_FOUND";
241
244
  };
242
245
  EmailTemplateAlreadyExists: KnownErrorConstructor<KnownError & KnownErrorBrand<"EMAIL_TEMPLATE_ALREADY_EXISTS">, []> & {
243
246
  errorCode: "EMAIL_TEMPLATE_ALREADY_EXISTS";
244
247
  };
248
+ OAuthConnectionNotConnectedToUser: KnownErrorConstructor<KnownError & KnownErrorBrand<"OAUTH_CONNECTION_NOT_CONNECTED_TO_USER">, []> & {
249
+ errorCode: "OAUTH_CONNECTION_NOT_CONNECTED_TO_USER";
250
+ };
251
+ OAuthConnectionAlreadyConnectedToAnotherUser: KnownErrorConstructor<KnownError & KnownErrorBrand<"OAUTH_CONNECTION_ALREADY_CONNECTED_TO_ANOTHER_USER">, []> & {
252
+ errorCode: "OAUTH_CONNECTION_ALREADY_CONNECTED_TO_ANOTHER_USER";
253
+ };
254
+ OAuthConnectionDoesNotHaveRequiredScope: KnownErrorConstructor<KnownError & KnownErrorBrand<"OAUTH_CONNECTION_DOES_NOT_HAVE_REQUIRED_SCOPE">, []> & {
255
+ errorCode: "OAUTH_CONNECTION_DOES_NOT_HAVE_REQUIRED_SCOPE";
256
+ };
257
+ OAuthExtraScopeNotAvailableWithSharedOAuthKeys: KnownErrorConstructor<KnownError & KnownErrorBrand<"OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS">, []> & {
258
+ errorCode: "OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS";
259
+ };
260
+ OAuthAccessTokenNotAvailableWithSharedOAuthKeys: KnownErrorConstructor<KnownError & KnownErrorBrand<"OAUTH_ACCESS_TOKEN_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS">, []> & {
261
+ errorCode: "OAUTH_ACCESS_TOKEN_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS";
262
+ };
263
+ UserAlreadyConnectedToAnotherOAuthConnection: KnownErrorConstructor<KnownError & KnownErrorBrand<"USER_ALREADY_CONNECTED_TO_ANOTHER_OAUTH_CONNECTION">, []> & {
264
+ errorCode: "USER_ALREADY_CONNECTED_TO_ANOTHER_OAUTH_CONNECTION";
265
+ };
245
266
  };
246
267
  export {};
@@ -341,6 +341,30 @@ const EmailTemplateAlreadyExists = createKnownErrorConstructor(KnownError, "EMAI
341
341
  400,
342
342
  "Email template already exists.",
343
343
  ], () => []);
344
+ const OAuthConnectionNotConnectedToUser = createKnownErrorConstructor(KnownError, "OAUTH_CONNECTION_NOT_CONNECTED_TO_USER", () => [
345
+ 400,
346
+ "The OAuth connection is not connected to any user.",
347
+ ], () => []);
348
+ const OAuthConnectionAlreadyConnectedToAnotherUser = createKnownErrorConstructor(KnownError, "OAUTH_CONNECTION_ALREADY_CONNECTED_TO_ANOTHER_USER", () => [
349
+ 400,
350
+ "The OAuth connection is already connected to another user.",
351
+ ], () => []);
352
+ const OAuthConnectionDoesNotHaveRequiredScope = createKnownErrorConstructor(KnownError, "OAUTH_CONNECTION_DOES_NOT_HAVE_REQUIRED_SCOPE", () => [
353
+ 400,
354
+ "The OAuth connection does not have the required scope.",
355
+ ], () => []);
356
+ const OAuthExtraScopeNotAvailableWithSharedOAuthKeys = createKnownErrorConstructor(KnownError, "OAUTH_EXTRA_SCOPE_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS", () => [
357
+ 400,
358
+ "Extra scopes are not available with shared OAuth keys. Please add your own OAuth keys on the Stack dashboard to use extra scopes.",
359
+ ], () => []);
360
+ const OAuthAccessTokenNotAvailableWithSharedOAuthKeys = createKnownErrorConstructor(KnownError, "OAUTH_ACCESS_TOKEN_NOT_AVAILABLE_WITH_SHARED_OAUTH_KEYS", () => [
361
+ 400,
362
+ "Access tokens are not available with shared OAuth keys. Please add your own OAuth keys on the Stack dashboard to use access tokens.",
363
+ ], () => []);
364
+ const UserAlreadyConnectedToAnotherOAuthConnection = createKnownErrorConstructor(KnownError, "USER_ALREADY_CONNECTED_TO_ANOTHER_OAUTH_CONNECTION", () => [
365
+ 400,
366
+ "The user is already connected to another OAuth account. Did you maybe selected the wrong account?",
367
+ ], () => []);
344
368
  export const KnownErrors = {
345
369
  UnsupportedError,
346
370
  BodyParsingError,
@@ -406,8 +430,15 @@ export const KnownErrors = {
406
430
  EmailAlreadyVerified,
407
431
  PermissionNotFound,
408
432
  PermissionScopeMismatch,
433
+ UserNotInTeam,
409
434
  TeamNotFound,
410
435
  EmailTemplateAlreadyExists,
436
+ OAuthConnectionNotConnectedToUser,
437
+ OAuthConnectionAlreadyConnectedToAnotherUser,
438
+ OAuthConnectionDoesNotHaveRequiredScope,
439
+ OAuthExtraScopeNotAvailableWithSharedOAuthKeys,
440
+ OAuthAccessTokenNotAvailableWithSharedOAuthKeys,
441
+ UserAlreadyConnectedToAnotherOAuthConnection,
411
442
  };
412
443
  // ensure that all known error codes are unique
413
444
  const knownErrorCodes = new Set();
@@ -32,7 +32,25 @@ export function suspend() {
32
32
  export function suspendIfSsr(caller) {
33
33
  if (!isBrowserLike()) {
34
34
  const error = Object.assign(new Error(deindent `
35
- ${caller ?? "This code path"} attempted to display a loading indicator during SSR by falling back to the nearest Suspense boundary. If you see this error, it means no Suspense boundary was found, and no loading indicator could be displayed. Make sure you are not catching this error with try-catch, and that the component is rendered inside a Suspense boundary, for example by adding a \`loading.tsx\` file in your app directory.
35
+ ${caller ?? "This code path"} attempted to display a loading indicator during SSR by falling back to the nearest Suspense boundary. If you see this error, it means no Suspense boundary was found, and no loading indicator could be displayed.
36
+
37
+ This usually has one of three causes:
38
+
39
+ 1. You are missing a loading.tsx file in your app directory. Fix it by adding a loading.tsx file in your app directory.
40
+
41
+ 2. The component is rendered in the root (outermost) layout.tsx or template.tsx file. Next.js does not wrap those files in a Suspense boundary, even if there is a loading.tsx file in the same folder. To fix it, wrap your layout inside a route group like this:
42
+
43
+ - app
44
+ - layout.tsx // contains <html> and <body>, alongside providers and other components that don't need ${caller ?? "this code path"}
45
+ - loading.tsx // required for suspense
46
+ - (main)
47
+ - layout.tsx // contains the main layout of your app, like a sidebar or a header, and can use ${caller ?? "this code path"}
48
+ - route.tsx // your actual main page
49
+ - the rest of your app
50
+
51
+ For more information on this approach, see Next's documentation on route groups: https://nextjs.org/docs/app/building-your-application/routing/route-groups
52
+
53
+ 3. You caught this error with try-catch or a custom error boundary. Fix this by rethrowing the error or not catching it in the first place.
36
54
 
37
55
  See: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
38
56
 
@@ -39,6 +39,8 @@ export declare function trimLines(s: string): string;
39
39
  export declare function templateIdentity(strings: TemplateStringsArray | readonly string[], ...values: any[]): string;
40
40
  export declare function deindent(code: string): string;
41
41
  export declare function deindent(strings: TemplateStringsArray | readonly string[], ...values: any[]): string;
42
+ export declare function extractScopes(scope: string, removeDuplicates?: boolean): string[];
43
+ export declare function mergeScopeStrings(...scopes: string[]): string;
42
44
  export declare function nicify(value: unknown, { depth }?: {
43
45
  depth?: number | undefined;
44
46
  }): string;
@@ -92,6 +92,16 @@ export function deindent(strings, ...values) {
92
92
  });
93
93
  return templateIdentity(deindentedStrings, ...indentedValues);
94
94
  }
95
+ export function extractScopes(scope, removeDuplicates = true) {
96
+ const trimmedString = scope.trim();
97
+ const scopesArray = trimmedString.split(/\s+/);
98
+ const filtered = scopesArray.filter(scope => scope.length > 0);
99
+ return removeDuplicates ? [...new Set(filtered)] : filtered;
100
+ }
101
+ export function mergeScopeStrings(...scopes) {
102
+ const allScope = scopes.map((s) => extractScopes(s)).flat().join(" ");
103
+ return extractScopes(allScope).join(" ");
104
+ }
95
105
  export function nicify(value, { depth = 5 } = {}) {
96
106
  switch (typeof value) {
97
107
  case "string":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.4.21",
3
+ "version": "2.4.22",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -26,7 +26,7 @@
26
26
  "jose": "^5.2.2",
27
27
  "oauth4webapi": "^2.10.3",
28
28
  "uuid": "^9.0.1",
29
- "@stackframe/stack-sc": "2.4.21"
29
+ "@stackframe/stack-sc": "2.4.22"
30
30
  },
31
31
  "devDependencies": {
32
32
  "rimraf": "^5.0.5",