@stackframe/stack-shared 2.6.34 → 2.6.37

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.6.37
4
+
5
+ ### Patch Changes
6
+
7
+ - Various changes
8
+ - @stackframe/stack-sc@2.6.37
9
+
10
+ ## 2.6.36
11
+
12
+ ### Patch Changes
13
+
14
+ - Various updates
15
+ - @stackframe/stack-sc@2.6.36
16
+
17
+ ## 2.6.35
18
+
19
+ ### Patch Changes
20
+
21
+ - Bugfixes
22
+ - @stackframe/stack-sc@2.6.35
23
+
3
24
  ## 2.6.34
4
25
 
5
26
  ### Patch Changes
@@ -1,4 +1,4 @@
1
- import { StackAssertionError } from "../utils/errors";
1
+ import { StackAssertionError, captureError } from "../utils/errors";
2
2
  import { isLocalhost } from "../utils/urls";
3
3
  export function getProductionModeErrors(project) {
4
4
  const errors = [];
@@ -15,10 +15,15 @@ export function getProductionModeErrors(project) {
15
15
  url = new URL(domain);
16
16
  }
17
17
  catch (e) {
18
- throw new StackAssertionError("Domain was somehow not a valid URL; we should've caught this when setting the domain in the first place", {
18
+ captureError("production-mode-domain-not-valid", new StackAssertionError("Domain was somehow not a valid URL; we should've caught this when setting the domain in the first place", {
19
19
  domain,
20
20
  projectId: project
21
+ }));
22
+ errors.push({
23
+ message: "Trusted domain is not a valid URL: " + domain,
24
+ relativeFixUrl: domainsFixUrl,
21
25
  });
26
+ continue;
22
27
  }
23
28
  if (isLocalhost(url)) {
24
29
  errors.push({
@@ -230,7 +230,7 @@ export class StackClientInterface {
230
230
  return Result.error(e);
231
231
  }
232
232
  else {
233
- throw this._createNetworkError(e, session, requestType);
233
+ throw await this._createNetworkError(e, session, requestType);
234
234
  }
235
235
  }
236
236
  throw e;
@@ -149,7 +149,7 @@ const AccessTypeWithoutProjectId = createKnownErrorConstructor(InvalidProjectAut
149
149
  deindent `
150
150
  The x-stack-access-type header was '${accessType}', but the x-stack-project-id header was not provided.
151
151
 
152
- For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/auth#authentication
152
+ For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
153
153
  `,
154
154
  {
155
155
  request_type: accessType,
@@ -160,7 +160,7 @@ const AccessTypeRequired = createKnownErrorConstructor(InvalidProjectAuthenticat
160
160
  deindent `
161
161
  You must specify an access level for this Stack project. Make sure project API keys are provided (eg. x-stack-publishable-client-key) and you set the x-stack-access-type header to 'client', 'server', or 'admin'.
162
162
 
163
- For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/auth#authentication
163
+ For more information, see the docs on REST API authentication: https://docs.stack-auth.com/rest-api/overview#authentication
164
164
  `,
165
165
  ], () => []);
166
166
  const InsufficientAccessType = createKnownErrorConstructor(InvalidProjectAuthentication, "INSUFFICIENT_ACCESS_TYPE", (actualAccessType, allowedAccessTypes) => [
@@ -33,16 +33,17 @@ export declare const jsonStringOrEmptySchema: yup.StringSchema<string | undefine
33
33
  export declare const base64Schema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
34
34
  export declare const passwordSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
35
35
  /**
36
- * A stricter email schema that does some additional checks for UX input.
36
+ * A stricter email schema that does some additional checks for UX input. (Some emails are allowed by the spec, for
37
+ * example `test@localhost` or `abc@gmail`, but almost certainly a user input error.)
37
38
  *
38
39
  * Note that some users in the DB have an email that doesn't match this regex, so most of the time you should use
39
40
  * `emailSchema` instead until we do the DB migration.
40
41
  */
41
42
  export declare const strictEmailSchema: (message: string | undefined) => yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
42
43
  export declare const emailSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
43
- export declare const clientOrHigherAuthTypeSchema: yup.StringSchema<"client" | "server" | "admin" | undefined, yup.AnyObject, undefined, "">;
44
- export declare const serverOrHigherAuthTypeSchema: yup.StringSchema<"server" | "admin" | undefined, yup.AnyObject, undefined, "">;
45
- export declare const adminAuthTypeSchema: yup.StringSchema<"admin" | undefined, yup.AnyObject, undefined, "">;
44
+ export declare const clientOrHigherAuthTypeSchema: yup.StringSchema<"client" | "server" | "admin", yup.AnyObject, undefined, "">;
45
+ export declare const serverOrHigherAuthTypeSchema: yup.StringSchema<"server" | "admin", yup.AnyObject, undefined, "">;
46
+ export declare const adminAuthTypeSchema: yup.StringSchema<"admin", yup.AnyObject, undefined, "">;
46
47
  export declare const projectIdSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
47
48
  export declare const projectDisplayNameSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
48
49
  export declare const projectDescriptionSchema: yup.StringSchema<string | null | undefined, yup.AnyObject, undefined, "">;
@@ -195,19 +195,20 @@ export const base64Schema = yupString().test("is-base64", (params) => `${params.
195
195
  });
196
196
  export const passwordSchema = yupString().max(70);
197
197
  /**
198
- * A stricter email schema that does some additional checks for UX input.
198
+ * A stricter email schema that does some additional checks for UX input. (Some emails are allowed by the spec, for
199
+ * example `test@localhost` or `abc@gmail`, but almost certainly a user input error.)
199
200
  *
200
201
  * Note that some users in the DB have an email that doesn't match this regex, so most of the time you should use
201
202
  * `emailSchema` instead until we do the DB migration.
202
203
  */
203
204
  // eslint-disable-next-line no-restricted-syntax
204
- export const strictEmailSchema = (message) => yupString().email(message).matches(/^.*@.*\..*$/, message);
205
+ export const strictEmailSchema = (message) => yupString().email(message).matches(/^.*@.*\.[^.][^.]+$/, message);
205
206
  // eslint-disable-next-line no-restricted-syntax
206
207
  export const emailSchema = yupString().email();
207
208
  // Request auth
208
- export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']);
209
- export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']);
210
- export const adminAuthTypeSchema = yupString().oneOf(['admin']);
209
+ export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']).defined();
210
+ export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']).defined();
211
+ export const adminAuthTypeSchema = yupString().oneOf(['admin']).defined();
211
212
  // Projects
212
213
  export const projectIdSchema = yupString().test((v) => v === undefined || v === "internal" || isUuid(v)).meta({ openapiField: { description: _idDescription('project'), exampleValue: 'e0b52f4d-dece-408c-af49-d23061bb0f8d' } });
213
214
  export const projectDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('project'), exampleValue: 'MyMusic' } });
@@ -27,9 +27,12 @@ export declare class InternalSession {
27
27
  private _accessToken;
28
28
  private readonly _refreshToken;
29
29
  /**
30
- * Whether the session as a whole is known to be invalid. Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
30
+ * Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
31
31
  *
32
- * Applies to both the access token and the refresh token (it is possible for the access token to be invalid but the refresh token to be valid, in which case the session is still valid).
32
+ * It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is
33
+ * still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to
34
+ * be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed
35
+ * in an access token, eg. in a server-side request handler).
33
36
  */
34
37
  private _knownToBeInvalid;
35
38
  private _refreshPromise;
package/dist/sessions.js CHANGED
@@ -25,14 +25,21 @@ export class InternalSession {
25
25
  constructor(_options) {
26
26
  this._options = _options;
27
27
  /**
28
- * Whether the session as a whole is known to be invalid. Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
28
+ * Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
29
29
  *
30
- * Applies to both the access token and the refresh token (it is possible for the access token to be invalid but the refresh token to be valid, in which case the session is still valid).
30
+ * It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is
31
+ * still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to
32
+ * be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed
33
+ * in an access token, eg. in a server-side request handler).
31
34
  */
32
35
  this._knownToBeInvalid = new Store(false);
33
36
  this._refreshPromise = null;
34
37
  this._accessToken = new Store(_options.accessToken ? new AccessToken(_options.accessToken) : null);
35
38
  this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;
39
+ if (_options.accessToken === null && _options.refreshToken === null) {
40
+ // this session is already invalid
41
+ this._knownToBeInvalid.set(true);
42
+ }
36
43
  this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });
37
44
  }
38
45
  static calculateSessionKey(ofTokens) {
@@ -0,0 +1,6 @@
1
+ export declare function getBrowserCompatibilityReport(): {
2
+ optionalChaining: string | boolean;
3
+ nullishCoalescing: string | boolean;
4
+ weakRef: string | boolean;
5
+ cryptoUuid: string | boolean;
6
+ };
@@ -0,0 +1,17 @@
1
+ export function getBrowserCompatibilityReport() {
2
+ const test = (snippet) => {
3
+ try {
4
+ (0, eval)(snippet);
5
+ return true;
6
+ }
7
+ catch (e) {
8
+ return `FAILED: ${e}`;
9
+ }
10
+ };
11
+ return {
12
+ optionalChaining: test("({})?.b?.c"),
13
+ nullishCoalescing: test("0 ?? 1"),
14
+ weakRef: test("new WeakRef({})"),
15
+ cryptoUuid: test("crypto.randomUUID()"),
16
+ };
17
+ }
@@ -3,11 +3,11 @@ 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
5
  /**
6
- * Concatenates the stacktraces of the given errors onto the first.
6
+ * Concatenates the (original) stacktraces of the given errors onto the first.
7
7
  *
8
8
  * Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
9
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,
10
+ * but if you don't, the stacktrace will be lost.
11
11
  *
12
12
  * Here's an example of the unwanted behavior:
13
13
  *
@@ -24,16 +24,11 @@ export declare function throwErr(...args: StatusErrorConstructorParameters): nev
24
24
  * ```
25
25
  */
26
26
  export declare function concatStacktraces(first: Error, ...errors: Error[]): void;
27
- export declare class StackAssertionError extends Error implements ErrorWithCustomCapture {
28
- readonly extraData?: Record<string, any> | undefined;
29
- constructor(message: string, extraData?: Record<string, any> | undefined, options?: ErrorOptions);
30
- customCaptureExtraArgs: {
31
- cause?: {} | undefined;
32
- }[];
27
+ export declare class StackAssertionError extends Error {
28
+ readonly extraData?: (Record<string, any> & ErrorOptions) | undefined;
29
+ constructor(message: string, extraData?: (Record<string, any> & ErrorOptions) | undefined);
33
30
  }
34
- export type ErrorWithCustomCapture = {
35
- customCaptureExtraArgs: any[];
36
- };
31
+ export declare function errorToNiceString(error: unknown): string;
37
32
  export declare function registerErrorSink(sink: (location: string, error: unknown) => void): void;
38
33
  export declare function captureError(location: string, error: unknown): void;
39
34
  type Status = {
@@ -1,4 +1,5 @@
1
1
  import { globalVar } from "./globals";
2
+ import { pick } from "./objects";
2
3
  export function throwErr(...args) {
3
4
  if (typeof args[0] === "string") {
4
5
  throw new StackAssertionError(args[0], args[1]);
@@ -17,11 +18,11 @@ function removeStacktraceNameLine(stack) {
17
18
  return stack.split("\n").slice(addsNameLine ? 1 : 0).join("\n");
18
19
  }
19
20
  /**
20
- * Concatenates the stacktraces of the given errors onto the first.
21
+ * Concatenates the (original) stacktraces of the given errors onto the first.
21
22
  *
22
23
  * Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart
23
24
  * 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
+ * but if you don't, the stacktrace will be lost.
25
26
  *
26
27
  * Here's an example of the unwanted behavior:
27
28
  *
@@ -50,19 +51,28 @@ export function concatStacktraces(first, ...errors) {
50
51
  }
51
52
  }
52
53
  export class StackAssertionError extends Error {
53
- constructor(message, extraData, options) {
54
+ constructor(message, extraData) {
54
55
  const disclaimer = `\n\nThis is likely an error in Stack. Please make sure you are running the newest version and report it.`;
55
- super(`${message}${message.endsWith(disclaimer) ? "" : disclaimer}`, options);
56
+ super(`${message}${message.endsWith(disclaimer) ? "" : disclaimer}`, pick(extraData ?? {}, ["cause"]));
56
57
  this.extraData = extraData;
57
- this.customCaptureExtraArgs = [
58
- {
59
- ...this.extraData,
60
- ...this.cause ? { cause: this.cause } : {},
58
+ Object.defineProperty(this, "customCaptureExtraArgs", {
59
+ get() {
60
+ return [this.extraData];
61
61
  },
62
- ];
62
+ enumerable: false,
63
+ });
63
64
  }
64
65
  }
65
66
  StackAssertionError.prototype.name = "StackAssertionError";
67
+ export function errorToNiceString(error) {
68
+ if (!(error instanceof Error))
69
+ return `${typeof error}<${error}>`;
70
+ const stack = error.stack ?? "";
71
+ const toString = error.toString();
72
+ if (stack.startsWith(toString))
73
+ return stack;
74
+ return `${toString}\n${stack}`;
75
+ }
66
76
  const errorSinks = new Set();
67
77
  export function registerErrorSink(sink) {
68
78
  if (errorSinks.has(sink)) {
@@ -79,7 +89,10 @@ registerErrorSink((location, error, ...extraArgs) => {
79
89
  });
80
90
  export function captureError(location, error) {
81
91
  for (const sink of errorSinks) {
82
- sink(location, error, ...error && (typeof error === 'object' || typeof error === 'function') && "customCaptureExtraArgs" in error && Array.isArray(error.customCaptureExtraArgs) ? error.customCaptureExtraArgs : []);
92
+ sink(location,
93
+ // HACK: Log a nicified version of the error instead of statusError to get around buggy Next.js pretty-printing
94
+ // https://www.reddit.com/r/nextjs/comments/1gkxdqe/comment/m19kxgn/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
95
+ errorToNiceString(error), ...error && (typeof error === 'object' || typeof error === 'function') && "customCaptureExtraArgs" in error && Array.isArray(error.customCaptureExtraArgs) ? error.customCaptureExtraArgs : []);
83
96
  }
84
97
  }
85
98
  export class StatusError extends Error {
@@ -92,7 +105,7 @@ export class StatusError extends Error {
92
105
  this.name = "StatusError";
93
106
  this.statusCode = status;
94
107
  if (!message) {
95
- throw new StackAssertionError("StatusError always requires a message unless a Status object is passed", {}, { cause: this });
108
+ throw new StackAssertionError("StatusError always requires a message unless a Status object is passed", { cause: this });
96
109
  }
97
110
  }
98
111
  isClientError() {
package/dist/utils/jwt.js CHANGED
@@ -3,6 +3,7 @@ import elliptic from "elliptic";
3
3
  import * as jose from "jose";
4
4
  import { JOSEError } from "jose/errors";
5
5
  import { encodeBase64Url } from "./bytes";
6
+ import { StackAssertionError } from "./errors";
6
7
  import { globalVar } from "./globals";
7
8
  import { pick } from "./objects";
8
9
  const STACK_SERVER_SECRET = process.env.STACK_SERVER_SECRET ?? "";
@@ -74,6 +75,9 @@ export async function getPublicJwkSet(secretOrPrivateJwk) {
74
75
  };
75
76
  }
76
77
  export function getPerAudienceSecret(options) {
78
+ if (options.audience === "kid") {
79
+ throw new StackAssertionError("You cannot use the 'kid' audience for a per-audience secret, see comment below in jwt.tsx");
80
+ }
77
81
  return jose.base64url.encode(crypto
78
82
  .createHash('sha256')
79
83
  // TODO we should prefix a string like "stack-audience-secret" before we hash so you can't use `getKid(...)` to get the secret for eg. the "kid" audience if the same secret value is used
@@ -69,7 +69,7 @@ declare class RetryError extends AggregateError {
69
69
  constructor(errors: unknown[]);
70
70
  get retries(): number;
71
71
  }
72
- declare function retry<T>(fn: () => Result<T> | Promise<Result<T>>, retries: number, { exponentialDelayBase }: {
72
+ declare function retry<T>(fn: (attempt: number) => Result<T> | Promise<Result<T>>, totalAttempts: number, { exponentialDelayBase }?: {
73
73
  exponentialDelayBase?: number | undefined;
74
74
  }): Promise<Result<T, RetryError>>;
75
75
  export {};
@@ -98,7 +98,7 @@ class RetryError extends AggregateError {
98
98
  const strings = errors.map(e => String(e));
99
99
  const isAllSame = strings.length > 1 && strings.every(s => s === strings[0]);
100
100
  super(errors, deindent `
101
- Error after retrying ${errors.length} times.
101
+ Error after ${errors.length} attempts.
102
102
 
103
103
  ${isAllSame ? deindent `
104
104
  Attempts 1-${errors.length}:
@@ -116,17 +116,17 @@ class RetryError extends AggregateError {
116
116
  }
117
117
  }
118
118
  RetryError.prototype.name = "RetryError";
119
- async function retry(fn, retries, { exponentialDelayBase = 2000 }) {
119
+ async function retry(fn, totalAttempts, { exponentialDelayBase = 1000 } = {}) {
120
120
  const errors = [];
121
- for (let i = 0; i < retries; i++) {
122
- const res = await fn();
121
+ for (let i = 0; i < totalAttempts; i++) {
122
+ const res = await fn(i);
123
123
  if (res.status === "ok") {
124
124
  return Result.ok(res.data);
125
125
  }
126
126
  else {
127
127
  errors.push(res.error);
128
- if (i < retries - 1) {
129
- await wait(Math.random() * exponentialDelayBase * 2 ** i);
128
+ if (i < totalAttempts - 1) {
129
+ await wait((Math.random() + 0.5) * exponentialDelayBase * (2 ** i));
130
130
  }
131
131
  }
132
132
  }
@@ -0,0 +1,2 @@
1
+ import * as Sentry from "@sentry/nextjs";
2
+ export declare const sentryBaseConfig: Sentry.BrowserOptions & Sentry.NodeOptions & Sentry.VercelEdgeOptions;
@@ -0,0 +1,15 @@
1
+ export const sentryBaseConfig = {
2
+ ignoreErrors: [
3
+ // React throws these errors when used with some browser extensions (eg. Google Translate)
4
+ "NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.",
5
+ "NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.",
6
+ ],
7
+ normalizeDepth: 5,
8
+ maxValueLength: 5000,
9
+ // Adjust this value in production, or use tracesSampler for greater control
10
+ tracesSampleRate: 1.0,
11
+ // Setting this option to true will print useful information to the console while you're setting up Sentry.
12
+ debug: false,
13
+ replaysOnErrorSampleRate: 1.0,
14
+ replaysSessionSampleRate: 1.0,
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.6.34",
3
+ "version": "2.6.37",
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.34"
53
+ "@stackframe/stack-sc": "2.6.37"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@simplewebauthn/types": "^11.0.0",
@@ -61,7 +61,8 @@
61
61
  "rimraf": "^5.0.5",
62
62
  "react": "^18.2",
63
63
  "react-dom": "^18.2",
64
- "next": "^14.1.0"
64
+ "next": "^14.1.0",
65
+ "@sentry/nextjs": "^8.40.0"
65
66
  },
66
67
  "scripts": {
67
68
  "build": "tsc",