@stackframe/stack-shared 2.6.26 → 2.6.27

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,12 @@
1
1
  # @stackframe/stack-shared
2
2
 
3
+ ## 2.6.27
4
+
5
+ ### Patch Changes
6
+
7
+ - Bugfixes
8
+ - @stackframe/stack-sc@2.6.27
9
+
3
10
  ## 2.6.26
4
11
 
5
12
  ### Patch Changes
@@ -135,6 +135,7 @@ export declare const currentUserCrud: import("../../crud").CrudSchemaFromOptions
135
135
  primary_email_auth_enabled: undefined;
136
136
  passkey_auth_enabled: undefined;
137
137
  password: undefined;
138
+ password_hash: undefined;
138
139
  otp_auth_enabled: undefined;
139
140
  totp_secret_base64: undefined;
140
141
  selected_team_id: undefined;
@@ -150,6 +151,7 @@ export declare const currentUserCrud: import("../../crud").CrudSchemaFromOptions
150
151
  primary_email_auth_enabled: boolean | undefined;
151
152
  passkey_auth_enabled: boolean | undefined;
152
153
  password: string | null | undefined;
154
+ password_hash: string | undefined;
153
155
  otp_auth_enabled: boolean | undefined;
154
156
  totp_secret_base64: string | null | undefined;
155
157
  selected_team_id: string | null | undefined;
@@ -164,6 +166,7 @@ export declare const currentUserCrud: import("../../crud").CrudSchemaFromOptions
164
166
  primary_email_auth_enabled: undefined;
165
167
  passkey_auth_enabled: undefined;
166
168
  password: undefined;
169
+ password_hash: undefined;
167
170
  otp_auth_enabled: undefined;
168
171
  totp_secret_base64: undefined;
169
172
  selected_team_id: undefined;
@@ -10,6 +10,7 @@ export declare const usersCrudServerUpdateSchema: import("yup").ObjectSchema<{
10
10
  primary_email_auth_enabled: boolean | undefined;
11
11
  passkey_auth_enabled: boolean | undefined;
12
12
  password: string | null | undefined;
13
+ password_hash: string | undefined;
13
14
  otp_auth_enabled: boolean | undefined;
14
15
  totp_secret_base64: string | null | undefined;
15
16
  selected_team_id: string | null | undefined;
@@ -24,6 +25,7 @@ export declare const usersCrudServerUpdateSchema: import("yup").ObjectSchema<{
24
25
  primary_email_auth_enabled: undefined;
25
26
  passkey_auth_enabled: undefined;
26
27
  password: undefined;
28
+ password_hash: undefined;
27
29
  otp_auth_enabled: undefined;
28
30
  totp_secret_base64: undefined;
29
31
  selected_team_id: undefined;
@@ -100,6 +102,7 @@ export declare const usersCrudServerCreateSchema: import("yup").ObjectSchema<{
100
102
  primary_email_verified: boolean | undefined;
101
103
  primary_email_auth_enabled: boolean | undefined;
102
104
  passkey_auth_enabled: boolean | undefined;
105
+ password_hash: string | undefined;
103
106
  otp_auth_enabled: boolean | undefined;
104
107
  totp_secret_base64: string | null | undefined;
105
108
  } & {
@@ -119,6 +122,7 @@ export declare const usersCrudServerCreateSchema: import("yup").ObjectSchema<{
119
122
  primary_email_auth_enabled: undefined;
120
123
  passkey_auth_enabled: undefined;
121
124
  password: undefined;
125
+ password_hash: undefined;
122
126
  otp_auth_enabled: undefined;
123
127
  totp_secret_base64: undefined;
124
128
  selected_team_id: undefined;
@@ -198,6 +202,7 @@ export declare const usersCrud: import("../../crud").CrudSchemaFromOptions<{
198
202
  primary_email_auth_enabled: boolean | undefined;
199
203
  passkey_auth_enabled: boolean | undefined;
200
204
  password: string | null | undefined;
205
+ password_hash: string | undefined;
201
206
  otp_auth_enabled: boolean | undefined;
202
207
  totp_secret_base64: string | null | undefined;
203
208
  selected_team_id: string | null | undefined;
@@ -212,6 +217,7 @@ export declare const usersCrud: import("../../crud").CrudSchemaFromOptions<{
212
217
  primary_email_auth_enabled: undefined;
213
218
  passkey_auth_enabled: undefined;
214
219
  password: undefined;
220
+ password_hash: undefined;
215
221
  otp_auth_enabled: undefined;
216
222
  totp_secret_base64: undefined;
217
223
  selected_team_id: undefined;
@@ -227,6 +233,7 @@ export declare const usersCrud: import("../../crud").CrudSchemaFromOptions<{
227
233
  primary_email_verified: boolean | undefined;
228
234
  primary_email_auth_enabled: boolean | undefined;
229
235
  passkey_auth_enabled: boolean | undefined;
236
+ password_hash: string | undefined;
230
237
  otp_auth_enabled: boolean | undefined;
231
238
  totp_secret_base64: string | null | undefined;
232
239
  } & {
@@ -246,6 +253,7 @@ export declare const usersCrud: import("../../crud").CrudSchemaFromOptions<{
246
253
  primary_email_auth_enabled: undefined;
247
254
  passkey_auth_enabled: undefined;
248
255
  password: undefined;
256
+ password_hash: undefined;
249
257
  otp_auth_enabled: undefined;
250
258
  totp_secret_base64: undefined;
251
259
  selected_team_id: undefined;
@@ -12,6 +12,7 @@ export const usersCrudServerUpdateSchema = fieldSchema.yupObject({
12
12
  primary_email_auth_enabled: fieldSchema.primaryEmailAuthEnabledSchema.optional(),
13
13
  passkey_auth_enabled: fieldSchema.userOtpAuthEnabledSchema.optional(),
14
14
  password: fieldSchema.userPasswordMutationSchema.optional(),
15
+ password_hash: fieldSchema.userPasswordHashMutationSchema.optional(),
15
16
  otp_auth_enabled: fieldSchema.userOtpAuthEnabledMutationSchema.optional(),
16
17
  totp_secret_base64: fieldSchema.userTotpSecretMutationSchema.optional(),
17
18
  selected_team_id: fieldSchema.selectedTeamIdSchema.nullable().optional(),
@@ -20,10 +20,7 @@ export declare function yupMixed<A extends {}>(...args: Parameters<typeof yup.mi
20
20
  export declare function yupArray<A extends yup.Maybe<yup.AnyObject> = yup.AnyObject, B = any>(...args: Parameters<typeof yup.array<A, B>>): yup.ArraySchema<B[] | undefined, A, undefined, "">;
21
21
  export declare function yupTuple<T extends [unknown, ...unknown[]]>(...args: Parameters<typeof yup.tuple<T>>): yup.TupleSchema<T | undefined, yup.AnyObject, undefined, "">;
22
22
  export declare function yupObject<A extends yup.Maybe<yup.AnyObject>, B extends yup.ObjectShape>(...args: Parameters<typeof yup.object<A, B>>): yup.ObjectSchema<yup.TypeFromShape<B, yup.AnyObject> extends infer T ? T extends yup.TypeFromShape<B, yup.AnyObject> ? T extends {} ? { [k in keyof T]: T[k]; } : T : never : never, yup.AnyObject, yup.DefaultFromShape<B> extends infer T_1 ? T_1 extends yup.DefaultFromShape<B> ? T_1 extends {} ? { [k_1 in keyof T_1]: T_1[k_1]; } : T_1 : never : never, "">;
23
- /**
24
- * Note that the .defined() on unions is implicit.
25
- */
26
- export declare function yupUnion<T extends yup.ISchema<any>[]>(...args: T): yup.ISchema<yup.InferType<T[number]>>;
23
+ export declare function yupUnion<T extends yup.ISchema<any>[]>(...args: T): yup.MixedSchema<yup.InferType<T[number]>>;
27
24
  export declare const adaptSchema: yup.MixedSchema<typeof StackAdaptSentinel | undefined, yup.AnyObject, undefined, "">;
28
25
  /**
29
26
  * Yup's URL schema does not recognize some URLs (including `http://localhost`) as a valid URL. This schema is a workaround for that.
@@ -32,8 +29,16 @@ export declare const urlSchema: yup.StringSchema<string | undefined, yup.AnyObje
32
29
  export declare const jsonSchema: yup.MixedSchema<{} | null, yup.AnyObject, undefined, "">;
33
30
  export declare const jsonStringSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
34
31
  export declare const jsonStringOrEmptySchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
35
- export declare const emailSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
36
32
  export declare const base64Schema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
33
+ export declare const passwordSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
34
+ /**
35
+ * A stricter email schema that does some additional checks for UX input.
36
+ *
37
+ * Note that some users in the DB have an email that doesn't match this regex, so most of the time you should use
38
+ * `emailSchema` instead until we do the DB migration.
39
+ */
40
+ export declare const strictEmailSchema: (message: string | undefined) => yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
41
+ export declare const emailSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
37
42
  export declare const clientOrHigherAuthTypeSchema: yup.StringSchema<"client" | "server" | "admin" | undefined, yup.AnyObject, undefined, "">;
38
43
  export declare const serverOrHigherAuthTypeSchema: yup.StringSchema<"server" | "admin" | undefined, yup.AnyObject, undefined, "">;
39
44
  export declare const adminAuthTypeSchema: yup.StringSchema<"admin" | undefined, yup.AnyObject, undefined, "">;
@@ -97,6 +102,7 @@ export declare const userOtpAuthEnabledSchema: yup.BooleanSchema<boolean | undef
97
102
  export declare const userOtpAuthEnabledMutationSchema: yup.BooleanSchema<boolean | undefined, yup.AnyObject, undefined, "">;
98
103
  export declare const userHasPasswordSchema: yup.BooleanSchema<boolean | undefined, yup.AnyObject, undefined, "">;
99
104
  export declare const userPasswordMutationSchema: yup.StringSchema<string | null | undefined, yup.AnyObject, undefined, "">;
105
+ export declare const userPasswordHashMutationSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
100
106
  export declare const userTotpSecretMutationSchema: yup.StringSchema<string | null | undefined, yup.AnyObject, undefined, "">;
101
107
  export declare const signInEmailSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
102
108
  export declare const emailOtpSignInCallbackUrlSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
@@ -123,9 +123,6 @@ export function yupObject(...args) {
123
123
  // we don't want to update the type of `object` to have a default flag
124
124
  return object.default(undefined);
125
125
  }
126
- /**
127
- * Note that the .defined() on unions is implicit.
128
- */
129
126
  export function yupUnion(...args) {
130
127
  if (args.length === 0)
131
128
  throw new Error('yupUnion must have at least one schema');
@@ -136,7 +133,7 @@ export function yupUnion(...args) {
136
133
  if (desc.type !== firstDesc.type)
137
134
  throw new StackAssertionError(`yupUnion must have schemas of the same type (got: ${firstDesc.type} and ${desc.type})`, { first, schema, firstDesc, desc });
138
135
  }
139
- return yupMixed().defined().test('is-one-of', 'Invalid value', async (value, context) => {
136
+ return yupMixed().test('is-one-of', 'Invalid value', async (value, context) => {
140
137
  const errors = [];
141
138
  for (const schema of args) {
142
139
  try {
@@ -196,12 +193,22 @@ export const jsonStringOrEmptySchema = yupString().test("json", "Invalid JSON fo
196
193
  return false;
197
194
  }
198
195
  });
199
- export const emailSchema = yupString().email();
200
196
  export const base64Schema = yupString().test("is-base64", "Invalid base64 format", (value) => {
201
197
  if (value == null)
202
198
  return true;
203
199
  return isBase64(value);
204
200
  });
201
+ export const passwordSchema = yupString().max(70);
202
+ /**
203
+ * A stricter email schema that does some additional checks for UX input.
204
+ *
205
+ * Note that some users in the DB have an email that doesn't match this regex, so most of the time you should use
206
+ * `emailSchema` instead until we do the DB migration.
207
+ */
208
+ // eslint-disable-next-line no-restricted-syntax
209
+ export const strictEmailSchema = (message) => yupString().email(message).matches(/^.*@.*\..*$/, message);
210
+ // eslint-disable-next-line no-restricted-syntax
211
+ export const emailSchema = yupString().email();
205
212
  // Request auth
206
213
  export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']);
207
214
  export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']);
@@ -236,7 +243,7 @@ export const emailHostSchema = yupString().meta({ openapiField: { description: '
236
243
  export const emailPortSchema = yupNumber().meta({ openapiField: { description: 'Email port. Needs to be specified when using type="standard"', exampleValue: 587 } });
237
244
  export const emailUsernameSchema = yupString().meta({ openapiField: { description: 'Email username. Needs to be specified when using type="standard"', exampleValue: 'smtp-email' } });
238
245
  export const emailSenderEmailSchema = emailSchema.meta({ openapiField: { description: 'Email sender email. Needs to be specified when using type="standard"', exampleValue: 'example@your-domain.com' } });
239
- export const emailPasswordSchema = yupString().meta({ openapiField: { description: 'Email password. Needs to be specified when using type="standard"', exampleValue: 'your-email-password' } });
246
+ export const emailPasswordSchema = passwordSchema.meta({ openapiField: { description: 'Email password. Needs to be specified when using type="standard"', exampleValue: 'your-email-password' } });
240
247
  // Project domain config
241
248
  export const projectTrustedDomainSchema = yupString().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' } });
242
249
  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' } });
@@ -283,7 +290,10 @@ export const userPasskeyAuthEnabledSchema = yupBoolean().meta({ openapiField: {
283
290
  export const userOtpAuthEnabledSchema = yupBoolean().meta({ openapiField: { hidden: true, description: 'Whether the user has OTP/magic link enabled. ', exampleValue: true } });
284
291
  export const userOtpAuthEnabledMutationSchema = yupBoolean().meta({ openapiField: { hidden: true, description: 'Whether the user has OTP/magic link enabled. Note that only accounts with verified emails can sign-in with OTP.', exampleValue: true } });
285
292
  export const userHasPasswordSchema = yupBoolean().meta({ openapiField: { hidden: true, description: 'Whether the user has a password set. If the user does not have a password set, they will not be able to sign in with email/password.', exampleValue: true } });
286
- export const userPasswordMutationSchema = yupString().nullable().meta({ openapiField: { description: 'Sets the user\'s password. Doing so revokes all current sessions.', exampleValue: 'my-new-password' } });
293
+ export const userPasswordMutationSchema = passwordSchema.nullable().meta({ openapiField: { description: 'Sets the user\'s password. Doing so revokes all current sessions.', exampleValue: 'my-new-password' } }).max(70);
294
+ export const userPasswordHashMutationSchema = yupString()
295
+ .nonEmpty()
296
+ .meta({ openapiField: { description: 'If `password` is not given, sets the user\'s password hash to the given string in Modular Crypt Format (ex.: `$2a$10$VIhIOofSMqGdGlL4wzE//e.77dAQGqNtF/1dT7bqCrVtQuInWy2qi`). Doing so revokes all current sessions.' } }); // we don't set an exampleValue here because it's exclusive with the password field and having both would break the generated example
287
297
  export const userTotpSecretMutationSchema = base64Schema.nullable().meta({ openapiField: { description: 'Enables 2FA and sets a TOTP secret for the user. Set to null to disable 2FA.', exampleValue: 'dG90cC1zZWNyZXQ=' } });
288
298
  // Auth
289
299
  export const signInEmailSchema = emailSchema.meta({ openapiField: { description: 'The email to sign in with.', exampleValue: 'johndoe@example.com' } });
@@ -2,6 +2,27 @@ import { Json } from "./json";
2
2
  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
+ /**
6
+ * Concatenates the stacktraces of the given errors onto the first.
7
+ *
8
+ * Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
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,
11
+ *
12
+ * Here's an example of the unwanted behavior:
13
+ *
14
+ * ```tsx
15
+ * async function log() {
16
+ * await wait(0); // simulate an put the task on the event loop
17
+ * console.log(new Error().stack);
18
+ * }
19
+ *
20
+ * async function main() {
21
+ * await log(); // good; prints both "log" and "main" on the stacktrace
22
+ * log(); // bad; prints only "log" on the stacktrace
23
+ * }
24
+ * ```
25
+ */
5
26
  export declare function concatStacktraces(first: Error, ...errors: Error[]): void;
6
27
  export declare class StackAssertionError extends Error implements ErrorWithCustomCapture {
7
28
  readonly extraData?: Record<string, any> | undefined;
@@ -16,6 +16,27 @@ function removeStacktraceNameLine(stack) {
16
16
  const addsNameLine = new Error().stack?.startsWith("Error\n");
17
17
  return stack.split("\n").slice(addsNameLine ? 1 : 0).join("\n");
18
18
  }
19
+ /**
20
+ * Concatenates the stacktraces of the given errors onto the first.
21
+ *
22
+ * Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
23
+ * 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
+ *
26
+ * Here's an example of the unwanted behavior:
27
+ *
28
+ * ```tsx
29
+ * async function log() {
30
+ * await wait(0); // simulate an put the task on the event loop
31
+ * console.log(new Error().stack);
32
+ * }
33
+ *
34
+ * async function main() {
35
+ * await log(); // good; prints both "log" and "main" on the stacktrace
36
+ * log(); // bad; prints only "log" on the stacktrace
37
+ * }
38
+ * ```
39
+ */
19
40
  export function concatStacktraces(first, ...errors) {
20
41
  // some browsers (eg. Firefox) add an extra empty line at the end
21
42
  const addsEmptyLineAtEnd = first.stack?.endsWith("\n");
@@ -0,0 +1,4 @@
1
+ export declare function hashPassword(password: string): Promise<string>;
2
+ export declare function comparePassword(password: string, hash: string): Promise<boolean>;
3
+ export declare function isPasswordHashValid(hash: string): Promise<boolean>;
4
+ export declare function getPasswordHashAlgorithm(hash: string): Promise<"bcrypt" | undefined>;
@@ -0,0 +1,44 @@
1
+ import bcrypt from 'bcrypt';
2
+ import { StackAssertionError } from './errors';
3
+ export async function hashPassword(password) {
4
+ const passwordBytes = new TextEncoder().encode(password);
5
+ if (passwordBytes.length >= 72) {
6
+ throw new StackAssertionError(`Password is too long for bcrypt`, { len: passwordBytes.length });
7
+ }
8
+ const salt = await bcrypt.genSalt(10);
9
+ return await bcrypt.hash(password, salt);
10
+ }
11
+ export async function comparePassword(password, hash) {
12
+ switch (await getPasswordHashAlgorithm(hash)) {
13
+ case "bcrypt": {
14
+ return await bcrypt.compare(password, hash);
15
+ }
16
+ default: {
17
+ return false;
18
+ }
19
+ }
20
+ }
21
+ export async function isPasswordHashValid(hash) {
22
+ return !!(await getPasswordHashAlgorithm(hash));
23
+ }
24
+ export async function getPasswordHashAlgorithm(hash) {
25
+ if (typeof hash !== "string") {
26
+ throw new StackAssertionError(`Passed non-string value to getPasswordHashAlgorithm`, { hash });
27
+ }
28
+ if (hash.match(/^\$2[ayb]\$.{56}$/)) {
29
+ try {
30
+ if (bcrypt.getRounds(hash) > 16) {
31
+ return undefined;
32
+ }
33
+ await bcrypt.compare("any string", hash);
34
+ return "bcrypt";
35
+ }
36
+ catch (e) {
37
+ console.warn(`Error while checking bcrypt password hash. Assuming the hash is invalid`, e);
38
+ return undefined;
39
+ }
40
+ }
41
+ else {
42
+ return undefined;
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.6.26",
3
+ "version": "2.6.27",
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.26"
53
+ "@stackframe/stack-sc": "2.6.27"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@simplewebauthn/types": "^11.0.0",
@@ -1,2 +0,0 @@
1
- export declare function hashPassword(password: string): Promise<string>;
2
- export declare function comparePassword(password: string, hash: string): Promise<boolean>;
@@ -1,8 +0,0 @@
1
- import bcrypt from 'bcrypt';
2
- export async function hashPassword(password) {
3
- const salt = await bcrypt.genSalt(10);
4
- return await bcrypt.hash(password, salt);
5
- }
6
- export async function comparePassword(password, hash) {
7
- return await bcrypt.compare(password, hash);
8
- }