@stackframe/stack-shared 2.7.7 → 2.7.9

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,20 @@
1
1
  # @stackframe/stack-shared
2
2
 
3
+ ## 2.7.9
4
+
5
+ ### Patch Changes
6
+
7
+ - Various changes
8
+ - @stackframe/stack-sc@2.7.9
9
+
10
+ ## 2.7.8
11
+
12
+ ### Patch Changes
13
+
14
+ - Various changes
15
+ - Updated dependencies
16
+ - @stackframe/stack-sc@2.7.8
17
+
3
18
  ## 2.7.7
4
19
 
5
20
  ### Patch Changes
@@ -121,7 +121,7 @@ export declare const KnownErrors: {
121
121
  };
122
122
  AdminAccessTokenExpired: KnownErrorConstructor<KnownError & KnownErrorBrand<"PROJECT_AUTHENTICATION_ERROR"> & {
123
123
  constructorArgs: [statusCode: number, humanReadableMessage: string, details?: Json | undefined];
124
- } & KnownErrorBrand<"INVALID_PROJECT_AUTHENTICATION"> & KnownErrorBrand<"INVALID_ADMIN_ACCESS_TOKEN"> & KnownErrorBrand<"ADMIN_ACCESS_TOKEN_EXPIRED">, []> & {
124
+ } & KnownErrorBrand<"INVALID_PROJECT_AUTHENTICATION"> & KnownErrorBrand<"INVALID_ADMIN_ACCESS_TOKEN"> & KnownErrorBrand<"ADMIN_ACCESS_TOKEN_EXPIRED">, [expiredAt: Date | undefined]> & {
125
125
  errorCode: "ADMIN_ACCESS_TOKEN_EXPIRED";
126
126
  };
127
127
  InvalidProjectForAdminAccessToken: KnownErrorConstructor<KnownError & KnownErrorBrand<"PROJECT_AUTHENTICATION_ERROR"> & {
@@ -194,7 +194,7 @@ export declare const KnownErrors: {
194
194
  };
195
195
  AccessTokenExpired: KnownErrorConstructor<KnownError & KnownErrorBrand<"SESSION_AUTHENTICATION_ERROR"> & {
196
196
  constructorArgs: [statusCode: number, humanReadableMessage: string, details?: Json | undefined];
197
- } & KnownErrorBrand<"INVALID_SESSION_AUTHENTICATION"> & KnownErrorBrand<"INVALID_ACCESS_TOKEN"> & KnownErrorBrand<"ACCESS_TOKEN_EXPIRED">, []> & {
197
+ } & KnownErrorBrand<"INVALID_SESSION_AUTHENTICATION"> & KnownErrorBrand<"INVALID_ACCESS_TOKEN"> & KnownErrorBrand<"ACCESS_TOKEN_EXPIRED">, [expiredAt: Date | undefined]> & {
198
198
  errorCode: "ACCESS_TOKEN_EXPIRED";
199
199
  };
200
200
  InvalidProjectForAccessToken: KnownErrorConstructor<KnownError & KnownErrorBrand<"SESSION_AUTHENTICATION_ERROR"> & {
@@ -171,10 +171,11 @@ const UnparsableAdminAccessToken = createKnownErrorConstructor(InvalidAdminAcces
171
171
  401,
172
172
  "Admin access token is not parsable.",
173
173
  ], () => []);
174
- const AdminAccessTokenExpired = createKnownErrorConstructor(InvalidAdminAccessToken, "ADMIN_ACCESS_TOKEN_EXPIRED", () => [
174
+ const AdminAccessTokenExpired = createKnownErrorConstructor(InvalidAdminAccessToken, "ADMIN_ACCESS_TOKEN_EXPIRED", (expiredAt) => [
175
175
  401,
176
- "Admin access token has expired. Please refresh it and try again.",
177
- ], () => []);
176
+ `Admin access token has expired. Please refresh it and try again.${expiredAt ? ` (The access token expired at ${expiredAt.toISOString()}.)` : ""}`,
177
+ { expired_at_millis: expiredAt?.getTime() ?? null },
178
+ ], (json) => [json.expired_at_millis ?? undefined]);
178
179
  const InvalidProjectForAdminAccessToken = createKnownErrorConstructor(InvalidAdminAccessToken, "INVALID_PROJECT_FOR_ADMIN_ACCESS_TOKEN", () => [
179
180
  401,
180
181
  "Admin access tokens must be created on the internal project.",
@@ -240,10 +241,11 @@ const UnparsableAccessToken = createKnownErrorConstructor(InvalidAccessToken, "U
240
241
  401,
241
242
  "Access token is not parsable.",
242
243
  ], () => []);
243
- const AccessTokenExpired = createKnownErrorConstructor(InvalidAccessToken, "ACCESS_TOKEN_EXPIRED", () => [
244
+ const AccessTokenExpired = createKnownErrorConstructor(InvalidAccessToken, "ACCESS_TOKEN_EXPIRED", (expiredAt) => [
244
245
  401,
245
- "Access token has expired. Please refresh it and try again.",
246
- ], () => []);
246
+ `Access token has expired. Please refresh it and try again.${expiredAt ? ` (The access token expired at ${expiredAt.toISOString()}.)` : ""}`,
247
+ { expired_at_millis: expiredAt?.getTime() ?? null },
248
+ ], (json) => [json.expired_at_millis ?? undefined]);
247
249
  const InvalidProjectForAccessToken = createKnownErrorConstructor(InvalidAccessToken, "INVALID_PROJECT_FOR_ACCESS_TOKEN", () => [
248
250
  401,
249
251
  "Access token not valid for this project.",
@@ -4,6 +4,10 @@ declare module "yup" {
4
4
  nonEmpty(message?: string): StringSchema<TType, TContext, TDefault, TFlags>;
5
5
  empty(): StringSchema<TType, TContext, TDefault, TFlags>;
6
6
  }
7
+ interface Schema<TType, TContext, TDefault, TFlags> {
8
+ getNested<K extends keyof TType>(path: K): yup.Schema<TType[K], TContext, TDefault, TFlags>;
9
+ concat<U extends yup.AnySchema>(schema: U): yup.Schema<Omit<TType, keyof yup.InferType<U>> & yup.InferType<U>, TContext, TDefault, TFlags>;
10
+ }
7
11
  }
8
12
  export declare function yupValidate<S extends yup.ISchema<any>>(schema: S, obj: unknown, options?: yup.ValidateOptions & {
9
13
  currentUserId?: string | null;
@@ -23,6 +27,7 @@ export declare function yupTuple<T extends [unknown, ...unknown[]]>(...args: Par
23
27
  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, "">;
24
28
  export declare function yupNever(): yup.MixedSchema<never>;
25
29
  export declare function yupUnion<T extends yup.ISchema<any>[]>(...args: T): yup.MixedSchema<yup.InferType<T[number]>>;
30
+ export declare function ensureObjectSchema<T extends yup.AnyObject>(schema: yup.Schema<T>): yup.ObjectSchema<T> & typeof schema;
26
31
  export declare const adaptSchema: yup.MixedSchema<typeof StackAdaptSentinel | undefined, yup.AnyObject, undefined, "">;
27
32
  /**
28
33
  * Yup's URL schema does not recognize some URLs (including `http://localhost`) as a valid URL. This schema is a workaround for that.
@@ -13,6 +13,11 @@ yup.addMethod(yup.string, "nonEmpty", function (message) {
13
13
  return value !== "";
14
14
  });
15
15
  });
16
+ yup.addMethod(yup.Schema, "getNested", function (path) {
17
+ if (!path.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/))
18
+ throw new StackAssertionError(`yupSchema.getNested can currently only be used with alphanumeric keys. Fix this in the future. Provided key: ${path}`);
19
+ return yup.reach(this, path);
20
+ });
16
21
  export async function yupValidate(schema, obj, options) {
17
22
  try {
18
23
  return await schema.validate(obj, {
@@ -155,6 +160,11 @@ export function yupUnion(...args) {
155
160
  });
156
161
  });
157
162
  }
163
+ export function ensureObjectSchema(schema) {
164
+ if (!(schema instanceof yup.ObjectSchema))
165
+ throw new StackAssertionError(`assertObjectSchema: schema is not an ObjectSchema: ${schema.describe().type}`);
166
+ return schema;
167
+ }
158
168
  // Common
159
169
  export const adaptSchema = yupMixed();
160
170
  /**
@@ -73,9 +73,9 @@ export declare class InternalSession {
73
73
  /**
74
74
  * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
75
75
  *
76
- * The newly generated tokens are shortlived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
76
+ * The newly generated tokens are short-lived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
77
77
  *
78
- * In most cases, you should prefer `getOrFetchLikelyValidTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
78
+ * In most cases, you should prefer `getOrFetchLikelyValidTokens`.
79
79
  *
80
80
  * @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).
81
81
  */
@@ -95,11 +95,7 @@ export declare class InternalSession {
95
95
  */
96
96
  private _getPotentiallyInvalidAccessTokenIfAvailable;
97
97
  /**
98
- * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
99
- */
100
- private _getOrFetchPotentiallyInvalidAccessToken;
101
- /**
102
- * You should prefer `_getOrFetchAccessToken` in almost all cases.
98
+ * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.
103
99
  *
104
100
  * @returns A newly fetched access token (never read from cache), or null if the session either does not represent a user or the session is invalid.
105
101
  */
package/dist/sessions.js CHANGED
@@ -10,7 +10,7 @@ export class AccessToken {
10
10
  }
11
11
  get expiresAt() {
12
12
  const { exp } = jose.decodeJwt(this.token);
13
- if (!exp)
13
+ if (exp === undefined)
14
14
  return new Date(8640000000000000); // max date value
15
15
  return new Date(exp * 1000);
16
16
  }
@@ -107,9 +107,9 @@ export class InternalSession {
107
107
  /**
108
108
  * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
109
109
  *
110
- * The newly generated tokens are shortlived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
110
+ * The newly generated tokens are short-lived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
111
111
  *
112
- * In most cases, you should prefer `getOrFetchLikelyValidTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
112
+ * In most cases, you should prefer `getOrFetchLikelyValidTokens`.
113
113
  *
114
114
  * @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).
115
115
  */
@@ -133,30 +133,17 @@ export class InternalSession {
133
133
  * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.
134
134
  */
135
135
  _getPotentiallyInvalidAccessTokenIfAvailable() {
136
- const accessToken = this._accessToken.get();
137
- if (accessToken && !accessToken.isExpired())
138
- return accessToken;
139
- return null;
140
- }
141
- /**
142
- * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
143
- */
144
- async _getOrFetchPotentiallyInvalidAccessToken() {
145
136
  if (!this._refreshToken)
146
137
  return null;
147
138
  if (this.isKnownToBeInvalid())
148
139
  return null;
149
- const oldAccessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();
150
- if (oldAccessToken)
151
- return oldAccessToken;
152
- // refresh access token
153
- if (!this._refreshPromise) {
154
- this._refreshAndSetRefreshPromise(this._refreshToken);
155
- }
156
- return await this._refreshPromise;
140
+ const accessToken = this._accessToken.get();
141
+ if (accessToken && !accessToken.isExpired())
142
+ return accessToken;
143
+ return null;
157
144
  }
158
145
  /**
159
- * You should prefer `_getOrFetchAccessToken` in almost all cases.
146
+ * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.
160
147
  *
161
148
  * @returns A newly fetched access token (never read from cache), or null if the session either does not represent a user or the session is invalid.
162
149
  */
@@ -165,7 +152,9 @@ export class InternalSession {
165
152
  return null;
166
153
  if (this._knownToBeInvalid.get())
167
154
  return null;
168
- this._refreshAndSetRefreshPromise(this._refreshToken);
155
+ if (!this._refreshPromise) {
156
+ this._refreshAndSetRefreshPromise(this._refreshToken);
157
+ }
169
158
  return await this._refreshPromise;
170
159
  }
171
160
  _refreshAndSetRefreshPromise(refreshToken) {
@@ -0,0 +1,4 @@
1
+ export type Truthy<T> = T extends null | undefined | 0 | "" | false ? false : true;
2
+ export type Falsy<T> = T extends null | undefined | 0 | "" | false ? true : false;
3
+ export declare function isTruthy<T>(value: T): value is T & Truthy<T>;
4
+ export declare function isFalsy<T>(value: T): value is T & Falsy<T>;
@@ -0,0 +1,6 @@
1
+ export function isTruthy(value) {
2
+ return !!value;
3
+ }
4
+ export function isFalsy(value) {
5
+ return !value;
6
+ }
@@ -0,0 +1,4 @@
1
+ export declare function list(path: string): Promise<string[]>;
2
+ export declare function listRecursively(p: string, options?: {
3
+ excludeDirectories?: boolean;
4
+ }): Promise<string[]>;
@@ -0,0 +1,22 @@
1
+ import * as stackFs from "fs";
2
+ import * as path from "path";
3
+ export async function list(path) {
4
+ return await stackFs.promises.readdir(path);
5
+ }
6
+ export async function listRecursively(p, options = {}) {
7
+ const files = await list(p);
8
+ return [
9
+ ...(await Promise.all(files.map(async (fileName) => {
10
+ const filePath = path.join(p, fileName);
11
+ if ((await stackFs.promises.stat(filePath)).isDirectory()) {
12
+ return [
13
+ ...(await listRecursively(filePath, options)),
14
+ ...(options.excludeDirectories ? [] : [filePath]),
15
+ ];
16
+ }
17
+ else {
18
+ return [filePath];
19
+ }
20
+ }))).flat(),
21
+ ];
22
+ }
@@ -1,2 +1,3 @@
1
1
  export declare function prettyPrintWithMagnitudes(num: number): string;
2
2
  export declare function toFixedMax(num: number, maxDecimals: number): string;
3
+ export declare function numberCompare(a: number, b: number): number;
@@ -24,3 +24,6 @@ export function prettyPrintWithMagnitudes(num) {
24
24
  export function toFixedMax(num, maxDecimals) {
25
25
  return num.toFixed(maxDecimals).replace(/\.?0+$/, "");
26
26
  }
27
+ export function numberCompare(a, b) {
28
+ return Math.sign(a - b);
29
+ }
@@ -45,6 +45,7 @@ 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[];
47
47
  export declare function mergeScopeStrings(...scopes: string[]): string;
48
+ export declare function escapeTemplateLiteral(s: string): string;
48
49
  export type Nicifiable = {
49
50
  getNicifiableKeys?(): PropertyKey[];
50
51
  getNicifiedObjectExtraLines?(): string[];
@@ -103,15 +103,20 @@ export function deindent(strings, ...values) {
103
103
  return templateIdentity(deindentedStrings, ...indentedValues);
104
104
  }
105
105
  export function extractScopes(scope, removeDuplicates = true) {
106
+ // TODO what is this for? can we move this into the OAuth code in the backend?
106
107
  const trimmedString = scope.trim();
107
108
  const scopesArray = trimmedString.split(/\s+/);
108
109
  const filtered = scopesArray.filter(scope => scope.length > 0);
109
110
  return removeDuplicates ? [...new Set(filtered)] : filtered;
110
111
  }
111
112
  export function mergeScopeStrings(...scopes) {
113
+ // TODO what is this for? can we move this into the OAuth code in the backend?
112
114
  const allScope = scopes.map((s) => extractScopes(s)).flat().join(" ");
113
115
  return extractScopes(allScope).join(" ");
114
116
  }
117
+ export function escapeTemplateLiteral(s) {
118
+ return s.replaceAll("`", "\\`").replaceAll("\\", "\\\\").replaceAll("$", "\\$");
119
+ }
115
120
  /**
116
121
  * Some classes have different constructor names in different environments (eg. `Headers` is sometimes called `_Headers`,
117
122
  * so we create an object of overrides to handle these cases.
@@ -165,11 +170,27 @@ export function nicify(value, options = {}) {
165
170
  });
166
171
  };
167
172
  switch (typeof value) {
168
- case "string":
169
173
  case "boolean":
170
174
  case "number": {
171
175
  return JSON.stringify(value);
172
176
  }
177
+ case "string": {
178
+ const isDeindentable = (v) => deindent(v) === v && v.includes("\n");
179
+ const wrapInDeindent = (v) => deindent `
180
+ deindent\`
181
+ ${currentIndent + lineIndent}${escapeTemplateLiteral(value).replaceAll("\n", nl + lineIndent)}
182
+ ${currentIndent}\`
183
+ `;
184
+ if (isDeindentable(value)) {
185
+ return wrapInDeindent(value);
186
+ }
187
+ else if (value.endsWith("\n") && isDeindentable(value.slice(0, -1))) {
188
+ return wrapInDeindent(value.slice(0, -1)) + ' + "\\n"';
189
+ }
190
+ else {
191
+ return JSON.stringify(value);
192
+ }
193
+ }
173
194
  case "undefined": {
174
195
  return "undefined";
175
196
  }
@@ -205,7 +226,7 @@ export function nicify(value, options = {}) {
205
226
  }
206
227
  }
207
228
  if (value instanceof URL) {
208
- return `URL(${JSON.stringify(value.toString())})`;
229
+ return `URL(${nicify(value.toString())})`;
209
230
  }
210
231
  if (ArrayBuffer.isView(value)) {
211
232
  return `${value.constructor.name}([${value.toString()}])`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.7.7",
3
+ "version": "2.7.9",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -51,7 +51,7 @@
51
51
  "oauth4webapi": "^2.10.3",
52
52
  "semver": "^7.6.3",
53
53
  "uuid": "^9.0.1",
54
- "@stackframe/stack-sc": "2.7.7"
54
+ "@stackframe/stack-sc": "2.7.9"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sentry/nextjs": "^8.40.0",