@stackframe/stack-shared 2.6.25 → 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 +15 -0
- package/dist/interface/crud/current-user.d.ts +3 -0
- package/dist/interface/crud/users.d.ts +8 -0
- package/dist/interface/crud/users.js +1 -0
- package/dist/schema-fields.d.ts +11 -5
- package/dist/schema-fields.js +17 -7
- package/dist/utils/errors.d.ts +22 -0
- package/dist/utils/errors.js +39 -1
- package/dist/utils/hashes.d.ts +4 -0
- package/dist/utils/hashes.js +44 -0
- package/dist/utils/promises.js +4 -13
- package/dist/utils/react.d.ts +7 -0
- package/dist/utils/react.js +36 -32
- package/package.json +2 -2
- package/dist/utils/password.d.ts +0 -2
- package/dist/utils/password.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
10
|
+
## 2.6.26
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Various bugfixes
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
- @stackframe/stack-sc@2.6.26
|
|
17
|
+
|
|
3
18
|
## 2.6.25
|
|
4
19
|
|
|
5
20
|
### 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(),
|
package/dist/schema-fields.d.ts
CHANGED
|
@@ -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, "">;
|
package/dist/schema-fields.js
CHANGED
|
@@ -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().
|
|
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 =
|
|
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 =
|
|
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' } });
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -2,6 +2,28 @@ 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
|
+
*/
|
|
26
|
+
export declare function concatStacktraces(first: Error, ...errors: Error[]): void;
|
|
5
27
|
export declare class StackAssertionError extends Error implements ErrorWithCustomCapture {
|
|
6
28
|
readonly extraData?: Record<string, any> | undefined;
|
|
7
29
|
constructor(message: string, extraData?: Record<string, any> | undefined, options?: ErrorOptions);
|
package/dist/utils/errors.js
CHANGED
|
@@ -11,6 +11,44 @@ export function throwErr(...args) {
|
|
|
11
11
|
throw new StatusError(...args);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
function removeStacktraceNameLine(stack) {
|
|
15
|
+
// some browsers (eg. Chrome) prepend the stack with an extra line with the error name
|
|
16
|
+
const addsNameLine = new Error().stack?.startsWith("Error\n");
|
|
17
|
+
return stack.split("\n").slice(addsNameLine ? 1 : 0).join("\n");
|
|
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
|
+
*/
|
|
40
|
+
export function concatStacktraces(first, ...errors) {
|
|
41
|
+
// some browsers (eg. Firefox) add an extra empty line at the end
|
|
42
|
+
const addsEmptyLineAtEnd = first.stack?.endsWith("\n");
|
|
43
|
+
// Add a reference to this function itself so that we know that stacktraces were concatenated
|
|
44
|
+
// If you are coming here from a stacktrace, please know that the two parts before and after this line are different
|
|
45
|
+
// stacktraces that were concatenated with concatStacktraces
|
|
46
|
+
const separator = removeStacktraceNameLine(new Error().stack ?? "").split("\n")[0];
|
|
47
|
+
for (const error of errors) {
|
|
48
|
+
const toAppend = removeStacktraceNameLine(error.stack ?? "");
|
|
49
|
+
first.stack += (addsEmptyLineAtEnd ? "" : "\n") + separator + "\n" + toAppend;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
14
52
|
export class StackAssertionError extends Error {
|
|
15
53
|
constructor(message, extraData, options) {
|
|
16
54
|
const disclaimer = `\n\nThis is likely an error in Stack. Please make sure you are running the newest version and report it.`;
|
|
@@ -33,7 +71,7 @@ export function registerErrorSink(sink) {
|
|
|
33
71
|
errorSinks.add(sink);
|
|
34
72
|
}
|
|
35
73
|
registerErrorSink((location, ...args) => {
|
|
36
|
-
console.error(`\x1b[
|
|
74
|
+
console.error(`\x1b[41mCaptured error in ${location}:`, ...args, "\x1b[0m");
|
|
37
75
|
});
|
|
38
76
|
registerErrorSink((location, error, ...extraArgs) => {
|
|
39
77
|
globalVar.stackCapturedErrors = globalVar.stackCapturedErrors ?? [];
|
|
@@ -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/dist/utils/promises.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StackAssertionError, captureError } from "./errors";
|
|
1
|
+
import { StackAssertionError, captureError, concatStacktraces } from "./errors";
|
|
2
2
|
import { DependenciesMap } from "./maps";
|
|
3
3
|
import { Result } from "./results";
|
|
4
4
|
import { generateUuid } from "./uuids";
|
|
@@ -101,12 +101,6 @@ export async function wait(ms) {
|
|
|
101
101
|
export async function waitUntil(date) {
|
|
102
102
|
return await wait(date.getTime() - Date.now());
|
|
103
103
|
}
|
|
104
|
-
class ErrorDuringRunAsynchronously extends Error {
|
|
105
|
-
constructor() {
|
|
106
|
-
super("The error above originated in a runAsynchronously() call. Here is the stacktrace associated with it.");
|
|
107
|
-
this.name = "ErrorDuringRunAsynchronously";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
104
|
export function runAsynchronouslyWithAlert(...args) {
|
|
111
105
|
return runAsynchronously(args[0], {
|
|
112
106
|
...args[1],
|
|
@@ -120,13 +114,10 @@ export function runAsynchronously(promiseOrFunc, options = {}) {
|
|
|
120
114
|
if (typeof promiseOrFunc === "function") {
|
|
121
115
|
promiseOrFunc = promiseOrFunc();
|
|
122
116
|
}
|
|
123
|
-
const duringError = new
|
|
117
|
+
const duringError = new Error();
|
|
124
118
|
promiseOrFunc?.catch(error => {
|
|
125
|
-
const newError = new StackAssertionError("Uncaught error in asynchronous function: " + error.toString(), {
|
|
126
|
-
|
|
127
|
-
}, {
|
|
128
|
-
cause: error,
|
|
129
|
-
});
|
|
119
|
+
const newError = new StackAssertionError("Uncaught error in asynchronous function: " + error.toString(), { cause: error });
|
|
120
|
+
concatStacktraces(newError, duringError);
|
|
130
121
|
options.onError?.(newError);
|
|
131
122
|
if (!options.noErrorLogging) {
|
|
132
123
|
captureError("runAsynchronously", newError);
|
package/dist/utils/react.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export declare function getNodeText(node: React.ReactNode): string;
|
|
|
9
9
|
* You can use this to translate older query- or AsyncResult-based code to new the Suspense system, for example: `if (query.isLoading) suspend();`
|
|
10
10
|
*/
|
|
11
11
|
export declare function suspend(): never;
|
|
12
|
+
export declare class NoSuspenseBoundaryError extends Error {
|
|
13
|
+
digest: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
constructor(options: {
|
|
16
|
+
caller?: string;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
12
19
|
/**
|
|
13
20
|
* Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error.
|
|
14
21
|
*/
|
package/dist/utils/react.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { isBrowserLike } from "./env";
|
|
2
3
|
import { neverResolve } from "./promises";
|
|
3
4
|
import { deindent } from "./strings";
|
|
4
|
-
import { isBrowserLike } from "./env";
|
|
5
5
|
export function forwardRefIfNeeded(render) {
|
|
6
6
|
// TODO: when we drop support for react 18, remove this
|
|
7
7
|
const version = React.version;
|
|
@@ -37,41 +37,45 @@ export function suspend() {
|
|
|
37
37
|
React.use(neverResolve());
|
|
38
38
|
throw new Error("Somehow a Promise that never resolves was resolved?");
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
This usually has one of three causes:
|
|
49
|
-
|
|
50
|
-
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
|
+
export class NoSuspenseBoundaryError extends Error {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
super(deindent `
|
|
43
|
+
${options.caller ?? "This code path"} attempted to display a loading indicator, but didn't find a Suspense boundary above it. Please read the error message below carefully.
|
|
44
|
+
|
|
45
|
+
The fix depends on which of the 3 scenarios caused it:
|
|
46
|
+
|
|
47
|
+
1. You are missing a loading.tsx file in your app directory. Fix it by adding a loading.tsx file in your app directory.
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
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:
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
- app
|
|
52
|
+
- - layout.tsx // contains <html> and <body>, alongside providers and other components that don't need ${options.caller ?? "this code path"}
|
|
53
|
+
- - loading.tsx // required for suspense
|
|
54
|
+
- - (main)
|
|
55
|
+
- - - layout.tsx // contains the main layout of your app, like a sidebar or a header, and can use ${options.caller ?? "this code path"}
|
|
56
|
+
- - - route.tsx // your actual main page
|
|
57
|
+
- - - the rest of your app
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
For more information on this approach, see Next's documentation on route groups: https://nextjs.org/docs/app/building-your-application/routing/route-groups
|
|
60
|
+
|
|
61
|
+
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.
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
See: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
More information on SSR and Suspense boundaries: https://react.dev/reference/react/Suspense#providing-a-fallback-for-server-errors-and-client-only-content
|
|
66
|
+
`);
|
|
67
|
+
this.name = "NoSuspenseBoundaryError";
|
|
68
|
+
this.reason = options.caller ?? "suspendIfSsr()";
|
|
69
|
+
// set the digest so nextjs doesn't log the error
|
|
70
|
+
// https://github.com/vercel/next.js/blob/d01d6d9c35a8c2725b3d74c1402ab76d4779a6cf/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts#L14
|
|
71
|
+
this.digest = "BAILOUT_TO_CLIENT_SIDE_RENDERING";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error.
|
|
76
|
+
*/
|
|
77
|
+
export function suspendIfSsr(caller) {
|
|
78
|
+
if (!isBrowserLike()) {
|
|
79
|
+
throw new NoSuspenseBoundaryError({ caller });
|
|
76
80
|
}
|
|
77
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackframe/stack-shared",
|
|
3
|
-
"version": "2.6.
|
|
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.
|
|
53
|
+
"@stackframe/stack-sc": "2.6.27"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@simplewebauthn/types": "^11.0.0",
|
package/dist/utils/password.d.ts
DELETED
package/dist/utils/password.js
DELETED
|
@@ -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
|
-
}
|