@stackframe/stack-shared 2.7.13 → 2.7.16

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,26 @@
1
1
  # @stackframe/stack-shared
2
2
 
3
+ ## 2.7.16
4
+
5
+ ### Patch Changes
6
+
7
+ - Various changes
8
+ - @stackframe/stack-sc@2.7.16
9
+
10
+ ## 2.7.15
11
+
12
+ ### Patch Changes
13
+
14
+ - Various changes
15
+ - @stackframe/stack-sc@2.7.15
16
+
17
+ ## 2.7.14
18
+
19
+ ### Patch Changes
20
+
21
+ - Various changes
22
+ - @stackframe/stack-sc@2.7.14
23
+
3
24
  ## 2.7.13
4
25
 
5
26
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  import { KnownErrors } from "..";
2
2
  const minLength = 8;
3
- const maxLength = 256;
3
+ const maxLength = 70;
4
4
  export function getPasswordError(password) {
5
5
  if (password.length < minLength) {
6
6
  return new KnownErrors.PasswordTooShort(minLength);
@@ -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
- baseUrl: string;
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.baseUrl + "/api/v1";
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.baseUrl,
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
- const url = this.getApiUrl() + path;
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,7 +280,7 @@ 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
286
  // See: https://github.com/stack-auth/stack/issues/319
@@ -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.baseUrl,
658
+ issuer: this.options.getBaseUrl(),
656
659
  algorithm: 'oauth2',
657
660
  token_endpoint: this.getApiUrl() + '/auth/oauth/token',
658
661
  };
@@ -45,7 +45,9 @@ export const emailConfigSchema = yupObject({
45
45
  }),
46
46
  });
47
47
  const domainSchema = yupObject({
48
- domain: schemaFields.projectTrustedDomainSchema.defined(),
48
+ domain: schemaFields.urlSchema.defined()
49
+ .matches(/^https?:\/\//, 'URL must start with http:// or https://')
50
+ .meta({ openapiField: { description: 'URL. Must start with http:// or https://', exampleValue: 'https://example.com' } }),
49
51
  handler_path: schemaFields.handlerPathSchema.defined(),
50
52
  });
51
53
  export const projectsCrudAdminReadSchema = yupObject({
@@ -59,7 +59,7 @@ export declare const teamsCrudClientCreateSchema: import("yup").ObjectSchema<{
59
59
  client_metadata: {} | null | undefined;
60
60
  } & {
61
61
  display_name: string;
62
- creator_user_id: string | undefined;
62
+ creator_user_id: string;
63
63
  }, import("yup").AnyObject, {
64
64
  display_name: undefined;
65
65
  profile_image_url: undefined;
@@ -114,7 +114,7 @@ export declare const teamsCrud: import("../../crud").CrudSchemaFromOptions<{
114
114
  client_metadata: {} | null | undefined;
115
115
  } & {
116
116
  display_name: string;
117
- creator_user_id: string | undefined;
117
+ creator_user_id: string;
118
118
  }, import("yup").AnyObject, {
119
119
  display_name: undefined;
120
120
  profile_image_url: undefined;
@@ -26,7 +26,7 @@ export const teamsCrudServerUpdateSchema = teamsCrudClientUpdateSchema.concat(yu
26
26
  // Create
27
27
  export const teamsCrudClientCreateSchema = teamsCrudClientUpdateSchema.concat(yupObject({
28
28
  display_name: fieldSchema.teamDisplayNameSchema.defined(),
29
- creator_user_id: fieldSchema.teamCreatorUserIdSchema.optional(),
29
+ creator_user_id: fieldSchema.teamCreatorUserIdSchema.defined(),
30
30
  }).defined());
31
31
  export const teamsCrudServerCreateSchema = teamsCrudServerUpdateSchema.concat(yupObject({
32
32
  display_name: fieldSchema.teamDisplayNameSchema.defined(),
@@ -7,7 +7,7 @@ export const usersCrudServerUpdateSchema = fieldSchema.yupObject({
7
7
  client_metadata: fieldSchema.userClientMetadataSchema.optional(),
8
8
  client_read_only_metadata: fieldSchema.userClientReadOnlyMetadataSchema.optional(),
9
9
  server_metadata: fieldSchema.userServerMetadataSchema.optional(),
10
- primary_email: fieldSchema.primaryEmailSchema.nullable().optional(),
10
+ primary_email: fieldSchema.primaryEmailSchema.nullable().optional().nonEmpty(),
11
11
  primary_email_verified: fieldSchema.primaryEmailVerifiedSchema.optional(),
12
12
  primary_email_auth_enabled: fieldSchema.primaryEmailAuthEnabledSchema.optional(),
13
13
  passkey_auth_enabled: fieldSchema.userOtpAuthEnabledSchema.optional(),
@@ -2,6 +2,7 @@ import { KnownErrors } from "../known-errors";
2
2
  import { StackAssertionError } from "../utils/errors";
3
3
  import { filterUndefined } from "../utils/objects";
4
4
  import { Result } from "../utils/results";
5
+ import { urlString } from "../utils/urls";
5
6
  import { StackClientInterface } from "./clientInterface";
6
7
  export class StackServerInterface extends StackClientInterface {
7
8
  constructor(options) {
@@ -57,35 +58,35 @@ export class StackServerInterface extends StackClientInterface {
57
58
  return user;
58
59
  }
59
60
  async getServerUserById(userId) {
60
- const response = await this.sendServerRequest(`/users/${userId}`, {}, null);
61
+ const response = await this.sendServerRequest(urlString `/users/${userId}`, {}, null);
61
62
  const user = await response.json();
62
63
  if (!user)
63
64
  return Result.error(new Error("Failed to get user"));
64
65
  return Result.ok(user);
65
66
  }
66
67
  async listServerTeamInvitations(options) {
67
- const response = await this.sendServerRequest("/team-invitations?team_id=" + options.teamId, {}, null);
68
+ const response = await this.sendServerRequest(urlString `/team-invitations?team_id=${options.teamId}`, {}, null);
68
69
  const result = await response.json();
69
70
  return result.items;
70
71
  }
71
72
  async revokeServerTeamInvitation(invitationId, teamId) {
72
- await this.sendServerRequest(`/team-invitations/${invitationId}?team_id=${teamId}`, { method: "DELETE" }, null);
73
+ await this.sendServerRequest(urlString `/team-invitations/${invitationId}?team_id=${teamId}`, { method: "DELETE" }, null);
73
74
  }
74
75
  async listServerTeamMemberProfiles(options) {
75
- const response = await this.sendServerRequest("/team-member-profiles?team_id=" + options.teamId, {}, null);
76
+ const response = await this.sendServerRequest(urlString `/team-member-profiles?team_id=${options.teamId}`, {}, null);
76
77
  const result = await response.json();
77
78
  return result.items;
78
79
  }
79
80
  async getServerTeamMemberProfile(options) {
80
- const response = await this.sendServerRequest(`/team-member-profiles/${options.teamId}/${options.userId}`, {}, null);
81
+ const response = await this.sendServerRequest(urlString `/team-member-profiles/${options.teamId}/${options.userId}`, {}, null);
81
82
  return await response.json();
82
83
  }
83
84
  async listServerTeamPermissions(options, session) {
84
- const response = await this.sendServerRequest("/team-permissions?" + new URLSearchParams(filterUndefined({
85
+ const response = await this.sendServerRequest(`/team-permissions?${new URLSearchParams(filterUndefined({
85
86
  user_id: options.userId,
86
87
  team_id: options.teamId,
87
88
  recursive: options.recursive.toString(),
88
- })), {}, session);
89
+ }))}`, {}, session);
89
90
  const result = await response.json();
90
91
  return result.items;
91
92
  }
@@ -107,9 +108,9 @@ export class StackServerInterface extends StackClientInterface {
107
108
  return await response.json();
108
109
  }
109
110
  async listServerTeams(options) {
110
- const response = await this.sendServerRequest("/teams?" + new URLSearchParams(filterUndefined({
111
+ const response = await this.sendServerRequest(`/teams?${new URLSearchParams(filterUndefined({
111
112
  user_id: options?.userId,
112
- })), {}, null);
113
+ }))}`, {}, null);
113
114
  const result = await response.json();
114
115
  return result.items;
115
116
  }
@@ -130,7 +131,7 @@ export class StackServerInterface extends StackClientInterface {
130
131
  return await response.json();
131
132
  }
132
133
  async updateServerTeam(teamId, data) {
133
- const response = await this.sendServerRequest(`/teams/${teamId}`, {
134
+ const response = await this.sendServerRequest(urlString `/teams/${teamId}`, {
134
135
  method: "PATCH",
135
136
  headers: {
136
137
  "content-type": "application/json",
@@ -140,10 +141,10 @@ export class StackServerInterface extends StackClientInterface {
140
141
  return await response.json();
141
142
  }
142
143
  async deleteServerTeam(teamId) {
143
- await this.sendServerRequest(`/teams/${teamId}`, { method: "DELETE" }, null);
144
+ await this.sendServerRequest(urlString `/teams/${teamId}`, { method: "DELETE" }, null);
144
145
  }
145
146
  async addServerUserToTeam(options) {
146
- const response = await this.sendServerRequest(`/team-memberships/${options.teamId}/${options.userId}`, {
147
+ const response = await this.sendServerRequest(urlString `/team-memberships/${options.teamId}/${options.userId}`, {
147
148
  method: "POST",
148
149
  headers: {
149
150
  "content-type": "application/json",
@@ -153,7 +154,7 @@ export class StackServerInterface extends StackClientInterface {
153
154
  return await response.json();
154
155
  }
155
156
  async removeServerUserFromTeam(options) {
156
- await this.sendServerRequest(`/team-memberships/${options.teamId}/${options.userId}`, {
157
+ await this.sendServerRequest(urlString `/team-memberships/${options.teamId}/${options.userId}`, {
157
158
  method: "DELETE",
158
159
  headers: {
159
160
  "content-type": "application/json",
@@ -162,7 +163,7 @@ export class StackServerInterface extends StackClientInterface {
162
163
  }, null);
163
164
  }
164
165
  async updateServerUser(userId, update) {
165
- const response = await this.sendServerRequest(`/users/${userId}`, {
166
+ const response = await this.sendServerRequest(urlString `/users/${userId}`, {
166
167
  method: "PATCH",
167
168
  headers: {
168
169
  "content-type": "application/json",
@@ -172,7 +173,7 @@ export class StackServerInterface extends StackClientInterface {
172
173
  return await response.json();
173
174
  }
174
175
  async createServerProviderAccessToken(userId, provider, scope) {
175
- const response = await this.sendServerRequest(`/connected-accounts/${userId}/${provider}/access-token`, {
176
+ const response = await this.sendServerRequest(urlString `/connected-accounts/${userId}/${provider}/access-token`, {
176
177
  method: "POST",
177
178
  headers: {
178
179
  "content-type": "application/json",
@@ -199,7 +200,7 @@ export class StackServerInterface extends StackClientInterface {
199
200
  };
200
201
  }
201
202
  async leaveServerTeam(options) {
202
- await this.sendClientRequest(`/team-memberships/${options.teamId}/${options.userId}`, {
203
+ await this.sendClientRequest(urlString `/team-memberships/${options.teamId}/${options.userId}`, {
203
204
  method: "DELETE",
204
205
  headers: {
205
206
  "content-type": "application/json",
@@ -208,7 +209,7 @@ export class StackServerInterface extends StackClientInterface {
208
209
  }, null);
209
210
  }
210
211
  async updateServerTeamMemberProfile(options) {
211
- await this.sendServerRequest(`/team-member-profiles/${options.teamId}/${options.userId}`, {
212
+ await this.sendServerRequest(urlString `/team-member-profiles/${options.teamId}/${options.userId}`, {
212
213
  method: "PATCH",
213
214
  headers: {
214
215
  "content-type": "application/json",
@@ -217,7 +218,7 @@ export class StackServerInterface extends StackClientInterface {
217
218
  }, null);
218
219
  }
219
220
  async grantServerTeamUserPermission(teamId, userId, permissionId) {
220
- await this.sendServerRequest(`/team-permissions/${teamId}/${userId}/${permissionId}`, {
221
+ await this.sendServerRequest(urlString `/team-permissions/${teamId}/${userId}/${permissionId}`, {
221
222
  method: "POST",
222
223
  headers: {
223
224
  "content-type": "application/json",
@@ -226,7 +227,7 @@ export class StackServerInterface extends StackClientInterface {
226
227
  }, null);
227
228
  }
228
229
  async revokeServerTeamUserPermission(teamId, userId, permissionId) {
229
- await this.sendServerRequest(`/team-permissions/${teamId}/${userId}/${permissionId}`, {
230
+ await this.sendServerRequest(urlString `/team-permissions/${teamId}/${userId}/${permissionId}`, {
230
231
  method: "DELETE",
231
232
  headers: {
232
233
  "content-type": "application/json",
@@ -235,7 +236,7 @@ export class StackServerInterface extends StackClientInterface {
235
236
  }, null);
236
237
  }
237
238
  async deleteServerServerUser(userId) {
238
- await this.sendServerRequest(`/users/${userId}`, {
239
+ await this.sendServerRequest(urlString `/users/${userId}`, {
239
240
  method: "DELETE",
240
241
  headers: {
241
242
  "content-type": "application/json",
@@ -254,7 +255,7 @@ export class StackServerInterface extends StackClientInterface {
254
255
  return await response.json();
255
256
  }
256
257
  async updateServerContactChannel(userId, contactChannelId, data) {
257
- const response = await this.sendServerRequest(`/contact-channels/${userId}/${contactChannelId}`, {
258
+ const response = await this.sendServerRequest(urlString `/contact-channels/${userId}/${contactChannelId}`, {
258
259
  method: "PATCH",
259
260
  headers: {
260
261
  "content-type": "application/json",
@@ -264,19 +265,19 @@ export class StackServerInterface extends StackClientInterface {
264
265
  return await response.json();
265
266
  }
266
267
  async deleteServerContactChannel(userId, contactChannelId) {
267
- await this.sendServerRequest(`/contact-channels/${userId}/${contactChannelId}`, {
268
+ await this.sendServerRequest(urlString `/contact-channels/${userId}/${contactChannelId}`, {
268
269
  method: "DELETE",
269
270
  }, null);
270
271
  }
271
272
  async listServerContactChannels(userId) {
272
- const response = await this.sendServerRequest(`/contact-channels?user_id=${userId}`, {
273
+ const response = await this.sendServerRequest(urlString `/contact-channels?user_id=${userId}`, {
273
274
  method: "GET",
274
275
  }, null);
275
276
  const json = await response.json();
276
277
  return json.items;
277
278
  }
278
279
  async sendServerContactChannelVerificationEmail(userId, contactChannelId, callbackUrl) {
279
- await this.sendServerRequest(`/contact-channels/${userId}/${contactChannelId}/send-verification-code`, {
280
+ await this.sendServerRequest(urlString `/contact-channels/${userId}/${contactChannelId}/send-verification-code`, {
280
281
  method: "POST",
281
282
  headers: {
282
283
  "content-type": "application/json",
@@ -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
- 400,
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
- 400,
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
- 400,
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
- 400,
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
- 400,
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
- 400,
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
- 400,
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
- 400,
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
- 400,
535
+ 409,
536
536
  `This ${type} is already used for authentication by another account.`,
537
537
  { type },
538
538
  ], (json) => [json.type]);
@@ -79,7 +79,6 @@ export declare const emailPortSchema: yup.NumberSchema<number | undefined, yup.A
79
79
  export declare const emailUsernameSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
80
80
  export declare const emailSenderEmailSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
81
81
  export declare const emailPasswordSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
82
- export declare const projectTrustedDomainSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
83
82
  export declare const handlerPathSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
84
83
  export declare class ReplaceFieldWithOwnUserId extends Error {
85
84
  readonly path: string;
@@ -248,12 +248,11 @@ export const oauthMicrosoftTenantIdSchema = yupString().meta({ openapiField: { d
248
248
  export const emailTypeSchema = yupString().oneOf(['shared', 'standard']).meta({ openapiField: { description: 'Email provider type, one of shared, standard. "shared" uses Stack shared email provider and it is only meant for development. "standard" uses your own email server and will have your email address as the sender.', exampleValue: 'standard' } });
249
249
  export const emailSenderNameSchema = yupString().meta({ openapiField: { description: 'Email sender name. Needs to be specified when using type="standard"', exampleValue: 'Stack' } });
250
250
  export const emailHostSchema = yupString().meta({ openapiField: { description: 'Email host. Needs to be specified when using type="standard"', exampleValue: 'smtp.your-domain.com' } });
251
- export const emailPortSchema = yupNumber().meta({ openapiField: { description: 'Email port. Needs to be specified when using type="standard"', exampleValue: 587 } });
251
+ export const emailPortSchema = yupNumber().min(0).max(65535).meta({ openapiField: { description: 'Email port. Needs to be specified when using type="standard"', exampleValue: 587 } });
252
252
  export const emailUsernameSchema = yupString().meta({ openapiField: { description: 'Email username. Needs to be specified when using type="standard"', exampleValue: 'smtp-email' } });
253
253
  export const emailSenderEmailSchema = emailSchema.meta({ openapiField: { description: 'Email sender email. Needs to be specified when using type="standard"', exampleValue: 'example@your-domain.com' } });
254
254
  export const emailPasswordSchema = passwordSchema.meta({ openapiField: { description: 'Email password. Needs to be specified when using type="standard"', exampleValue: 'your-email-password' } });
255
255
  // Project domain config
256
- export const projectTrustedDomainSchema = urlSchema.test('is-https', 'Trusted domain must start with https://', (value) => value?.startsWith('https://')).meta({ openapiField: { description: 'Your domain URL. Make sure you own and trust this domain. Needs to start with https://', exampleValue: 'https://example.com' } });
257
256
  export const handlerPathSchema = yupString().test('is-handler-path', 'Handler path must start with /', (value) => value?.startsWith('/')).meta({ openapiField: { description: 'Handler path. If you did not setup a custom handler path, it should be "/handler" by default. It needs to start with /', exampleValue: '/handler' } });
258
257
  // Users
259
258
  export class ReplaceFieldWithOwnUserId extends Error {
@@ -17,16 +17,16 @@ export declare class AsyncCache<D extends any[], T> {
17
17
  refreshWhere(predicate: (dependencies: D) => boolean): Promise<void>;
18
18
  readonly isCacheAvailable: (key: D) => boolean;
19
19
  readonly getIfCached: (key: D) => ({
20
+ status: "error";
21
+ error: unknown;
22
+ } & {
23
+ status: "error";
24
+ }) | ({
20
25
  status: "pending";
21
26
  } & {
22
27
  progress: void;
23
28
  } & {
24
29
  status: "pending";
25
- }) | ({
26
- status: "error";
27
- error: unknown;
28
- } & {
29
- status: "error";
30
30
  }) | ({
31
31
  status: "ok";
32
32
  data: T;
@@ -57,16 +57,16 @@ declare class AsyncValueCache<T> {
57
57
  });
58
58
  isCacheAvailable(): boolean;
59
59
  getIfCached(): ({
60
+ status: "error";
61
+ error: unknown;
62
+ } & {
63
+ status: "error";
64
+ }) | ({
60
65
  status: "pending";
61
66
  } & {
62
67
  progress: void;
63
68
  } & {
64
69
  status: "pending";
65
- }) | ({
66
- status: "error";
67
- error: unknown;
68
- } & {
69
- status: "error";
70
70
  }) | ({
71
71
  status: "ok";
72
72
  data: T;
@@ -5,3 +5,27 @@ export declare function isBrowserLike(): boolean;
5
5
  export declare function getEnvVariable(name: string, defaultValue?: string | undefined): string;
6
6
  export declare function getNextRuntime(): string;
7
7
  export declare function getNodeEnvironment(): string;
8
+ declare const _inlineEnvVars: {
9
+ readonly NEXT_PUBLIC_STACK_API_URL: {
10
+ readonly default: string | undefined;
11
+ readonly client: string | undefined;
12
+ readonly server: string | undefined;
13
+ };
14
+ readonly NEXT_PUBLIC_STACK_DASHBOARD_URL: {
15
+ readonly default: string | undefined;
16
+ readonly client: string | undefined;
17
+ readonly server: string | undefined;
18
+ };
19
+ readonly NEXT_PUBLIC_POSTHOG_KEY: string | undefined;
20
+ readonly NEXT_PUBLIC_STACK_SVIX_SERVER_URL: string | undefined;
21
+ readonly NEXT_PUBLIC_SENTRY_DSN: string | undefined;
22
+ readonly NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY: string | undefined;
23
+ readonly NEXT_PUBLIC_STACK_EMULATOR_ENABLED: string | undefined;
24
+ readonly NEXT_PUBLIC_STACK_EMULATOR_PROJECT_ID: string | undefined;
25
+ readonly NEXT_PUBLIC_STACK_PROJECT_ID: string | undefined;
26
+ readonly NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: string | undefined;
27
+ readonly NEXT_PUBLIC_STACK_URL: string | undefined;
28
+ readonly NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: string | undefined;
29
+ };
30
+ export declare function getPublicEnvVar(name: keyof typeof _inlineEnvVars): string | undefined;
31
+ export {};
package/dist/utils/env.js CHANGED
@@ -57,3 +57,82 @@ export function getNextRuntime() {
57
57
  export function getNodeEnvironment() {
58
58
  return getEnvVariable("NODE_ENV", "");
59
59
  }
60
+ // ===================== Hack to use dynamic env vars in docker build =====================
61
+ const _inlineEnvVars = {
62
+ NEXT_PUBLIC_STACK_API_URL: {
63
+ 'default': process.env.NEXT_PUBLIC_STACK_API_URL,
64
+ 'client': process.env.NEXT_PUBLIC_CLIENT_STACK_API_URL,
65
+ 'server': process.env.NEXT_PUBLIC_SERVER_STACK_API_URL,
66
+ },
67
+ NEXT_PUBLIC_STACK_DASHBOARD_URL: {
68
+ 'default': process.env.NEXT_PUBLIC_STACK_DASHBOARD_URL,
69
+ 'client': process.env.NEXT_PUBLIC_CLIENT_STACK_DASHBOARD_URL,
70
+ 'server': process.env.NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL,
71
+ },
72
+ NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
73
+ NEXT_PUBLIC_STACK_SVIX_SERVER_URL: process.env.NEXT_PUBLIC_STACK_SVIX_SERVER_URL,
74
+ NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
75
+ NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY: process.env.NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY,
76
+ NEXT_PUBLIC_STACK_EMULATOR_ENABLED: process.env.NEXT_PUBLIC_STACK_EMULATOR_ENABLED,
77
+ NEXT_PUBLIC_STACK_EMULATOR_PROJECT_ID: process.env.NEXT_PUBLIC_STACK_EMULATOR_PROJECT_ID,
78
+ NEXT_PUBLIC_STACK_PROJECT_ID: process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
79
+ NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
80
+ NEXT_PUBLIC_STACK_URL: process.env.NEXT_PUBLIC_STACK_URL,
81
+ NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: process.env.NEXT_PUBLIC_STACK_INBUCKET_WEB_URL,
82
+ };
83
+ // This will be replaced with the actual env vars after a docker build
84
+ const _postBuildEnvVars = {
85
+ NEXT_PUBLIC_STACK_API_URL: {
86
+ 'default': 'STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_API_URL',
87
+ 'client': 'STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_CLIENT_STACK_API_URL',
88
+ 'server': 'STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_SERVER_STACK_API_URL',
89
+ },
90
+ NEXT_PUBLIC_STACK_DASHBOARD_URL: {
91
+ 'default': 'STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_DASHBOARD_URL',
92
+ 'client': 'STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_CLIENT_STACK_DASHBOARD_URL',
93
+ 'server': 'STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_SERVER_STACK_DASHBOARD_URL',
94
+ },
95
+ NEXT_PUBLIC_STACK_PROJECT_ID: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_PROJECT_ID",
96
+ NEXT_PUBLIC_POSTHOG_KEY: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_POSTHOG_KEY",
97
+ NEXT_PUBLIC_STACK_SVIX_SERVER_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_SVIX_SERVER_URL",
98
+ NEXT_PUBLIC_SENTRY_DSN: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_SENTRY_DSN",
99
+ NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_VERSION_ALERTER_SEVERE_ONLY",
100
+ NEXT_PUBLIC_STACK_EMULATOR_ENABLED: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_EMULATOR_ENABLED",
101
+ NEXT_PUBLIC_STACK_EMULATOR_PROJECT_ID: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_EMULATOR_PROJECT_ID",
102
+ NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY",
103
+ NEXT_PUBLIC_STACK_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_URL",
104
+ NEXT_PUBLIC_STACK_INBUCKET_WEB_URL: "STACK_ENV_VAR_SENTINEL_NEXT_PUBLIC_STACK_INBUCKET_WEB_URL",
105
+ };
106
+ // If this is not replaced with "true", then we will not use inline env vars
107
+ const _usePostBuildEnvVars = 'STACK_ENV_VAR_SENTINEL_USE_INLINE_ENV_VARS';
108
+ export function getPublicEnvVar(name) {
109
+ // This is a hack to force the compiler not to do any smart optimizations
110
+ const _ = _usePostBuildEnvVars.toString() + _inlineEnvVars.toString(); // Force runtime evaluation
111
+ const value = _usePostBuildEnvVars.slice(0) === 'true' ? _postBuildEnvVars[name] : _inlineEnvVars[name];
112
+ // Helper function to check if a value is a sentinel
113
+ const isSentinel = (str) => {
114
+ return _usePostBuildEnvVars.slice(0) === 'true' && str && str.startsWith('STACK_ENV_VAR_SENTINEL');
115
+ };
116
+ // If it's a dictionary with client/server values
117
+ if (typeof value === 'object') {
118
+ const preferredValue = isBrowserLike() ? value.client : value.server;
119
+ // Check for sentinel values
120
+ if (isSentinel(preferredValue)) {
121
+ return isSentinel(value.default) ? undefined : value.default;
122
+ }
123
+ if (isSentinel(value.default)) {
124
+ return undefined;
125
+ }
126
+ return preferredValue || value.default;
127
+ }
128
+ else if (typeof value === 'string') {
129
+ if (isSentinel(value)) {
130
+ return undefined;
131
+ }
132
+ return value;
133
+ }
134
+ else {
135
+ return undefined;
136
+ }
137
+ }
138
+ // ======================================================================
@@ -1,3 +1,4 @@
1
+ import { KnownError } from "..";
1
2
  import { StackAssertionError, captureError, concatStacktraces } from "./errors";
2
3
  import { DependenciesMap } from "./maps";
3
4
  import { Result } from "./results";
@@ -105,7 +106,12 @@ export function runAsynchronouslyWithAlert(...args) {
105
106
  return runAsynchronously(args[0], {
106
107
  ...args[1],
107
108
  onError: error => {
108
- alert(`An unhandled error occurred. Please ${process.env.NODE_ENV === "development" ? `check the browser console for the full error.` : "report this to the developer."}\n\n${error}`);
109
+ if (error instanceof KnownError && process.env.NODE_ENV.includes("production")) {
110
+ alert(error.message);
111
+ }
112
+ else {
113
+ alert(`An unhandled error occurred. Please ${process.env.NODE_ENV === "development" ? `check the browser console for the full error.` : "report this to the developer."}\n\n${error}`);
114
+ }
109
115
  args[1]?.onError?.(error);
110
116
  },
111
117
  }, ...args.slice(2));
@@ -60,12 +60,6 @@ export declare class AsyncStore<T> implements ReadonlyAsyncStore<T> {
60
60
  isAvailable(): boolean;
61
61
  isRejected(): boolean;
62
62
  get(): ({
63
- status: "pending";
64
- } & {
65
- progress: void;
66
- } & {
67
- status: "pending";
68
- }) | ({
69
63
  status: "error";
70
64
  error: unknown;
71
65
  } & {
@@ -75,6 +69,12 @@ export declare class AsyncStore<T> implements ReadonlyAsyncStore<T> {
75
69
  data: T;
76
70
  } & {
77
71
  status: "ok";
72
+ }) | ({
73
+ status: "pending";
74
+ } & {
75
+ progress: void;
76
+ } & {
77
+ status: "pending";
78
78
  });
79
79
  getOrWait(): ReactPromise<T>;
80
80
  _setIfLatest(result: Result<T>, curCounter: number): boolean;
@@ -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: any[]): string;
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[];
@@ -61,25 +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) {
70
- if (strings.length === 0)
71
- return "";
72
82
  if (values.length !== strings.length - 1)
73
- throw new Error("Invalid number of values; must be one less than strings");
74
- return strings.slice(1).reduce((result, string, i) => `${result}${values[i] ?? "n/a"}${string}`, strings[0]);
83
+ throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
84
+ return strings.reduce((result, str, i) => result + str + (values[i] ?? ''), '');
75
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
+ });
76
102
  export function deindent(strings, ...values) {
77
103
  if (typeof strings === "string")
78
104
  return deindent([strings]);
79
105
  if (strings.length === 0)
80
106
  return "";
81
107
  if (values.length !== strings.length - 1)
82
- throw new Error("Invalid number of values; must be one less than strings");
108
+ throw new StackAssertionError("Invalid number of values; must be one less than strings", { strings, values });
83
109
  const trimmedStrings = [...strings];
84
110
  trimmedStrings[0] = trimEmptyLinesStart(trimmedStrings[0]);
85
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
+ });
@@ -1,5 +1,18 @@
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;
7
+ /**
8
+ * A template literal tag that returns a URL.
9
+ *
10
+ * Any values passed are encoded.
11
+ */
12
+ export declare function url(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): URL;
13
+ /**
14
+ * A template literal tag that returns a URL string.
15
+ *
16
+ * Any values passed are encoded.
17
+ */
18
+ export declare function urlString(strings: TemplateStringsArray | readonly string[], ...values: (string | number | boolean)[]): string;
@@ -1,4 +1,5 @@
1
1
  import { generateSecureRandomString } from "./crypto";
2
+ import { templateIdentity } from "./strings";
2
3
  export function createUrlIfValid(...args) {
3
4
  try {
4
5
  return new URL(...args);
@@ -10,6 +11,12 @@ export function createUrlIfValid(...args) {
10
11
  export function isValidUrl(url) {
11
12
  return !!createUrlIfValid(url);
12
13
  }
14
+ export function isValidHostname(hostname) {
15
+ const url = createUrlIfValid(`https://${hostname}`);
16
+ if (!url)
17
+ return false;
18
+ return url.hostname === hostname;
19
+ }
13
20
  export function isLocalhost(urlOrString) {
14
21
  const url = createUrlIfValid(urlOrString);
15
22
  if (!url)
@@ -34,3 +41,19 @@ export function isRelative(url) {
34
41
  export function getRelativePart(url) {
35
42
  return url.pathname + url.search + url.hash;
36
43
  }
44
+ /**
45
+ * A template literal tag that returns a URL.
46
+ *
47
+ * Any values passed are encoded.
48
+ */
49
+ export function url(strings, ...values) {
50
+ return new URL(urlString(strings, ...values));
51
+ }
52
+ /**
53
+ * A template literal tag that returns a URL string.
54
+ *
55
+ * Any values passed are encoded.
56
+ */
57
+ export function urlString(strings, ...values) {
58
+ return templateIdentity(strings, ...values.map(encodeURIComponent));
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.7.13",
3
+ "version": "2.7.16",
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.13"
55
+ "@stackframe/stack-sc": "2.7.16"
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 ."