@stackframe/stack-shared 2.4.13 → 2.4.20

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.
@@ -0,0 +1,146 @@
1
+ import { Store } from "./utils/stores";
2
+ export class AccessToken {
3
+ token;
4
+ constructor(token) {
5
+ this.token = token;
6
+ }
7
+ }
8
+ export class RefreshToken {
9
+ token;
10
+ constructor(token) {
11
+ this.token = token;
12
+ }
13
+ }
14
+ /**
15
+ * An InternalSession represents a user's session, which may or may not be valid. It may contain an access token, a refresh token, or both.
16
+ *
17
+ * A session never changes which user or session it belongs to, but the tokens in it may change over time.
18
+ */
19
+ export class InternalSession {
20
+ _options;
21
+ /**
22
+ * Each session has a session key that depends on the tokens inside. If the session has a refresh token, the session key depends only on the refresh token. If the session does not have a refresh token, the session key depends only on the access token.
23
+ *
24
+ * Multiple Session objects may have the same session key, which implies that they represent the same session by the same user. Furthermore, a session's key never changes over the lifetime of a session object.
25
+ *
26
+ * This is useful for caching and indexing sessions.
27
+ */
28
+ sessionKey;
29
+ /**
30
+ * An access token that is not known to be invalid (ie. may be valid, but may have expired).
31
+ */
32
+ _accessToken;
33
+ _refreshToken;
34
+ /**
35
+ * 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).
36
+ *
37
+ * 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).
38
+ */
39
+ _knownToBeInvalid = new Store(false);
40
+ _refreshPromise = null;
41
+ constructor(_options) {
42
+ this._options = _options;
43
+ this._accessToken = new Store(_options.accessToken ? new AccessToken(_options.accessToken) : null);
44
+ this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;
45
+ this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });
46
+ }
47
+ static calculateSessionKey(ofTokens) {
48
+ if (ofTokens.refreshToken) {
49
+ return `refresh-${ofTokens.refreshToken}`;
50
+ }
51
+ else if (ofTokens.accessToken) {
52
+ return `access-${ofTokens.accessToken}`;
53
+ }
54
+ else {
55
+ return "not-logged-in";
56
+ }
57
+ }
58
+ /**
59
+ * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.
60
+ */
61
+ markInvalid() {
62
+ this._accessToken.set(null);
63
+ this._knownToBeInvalid.set(true);
64
+ }
65
+ onInvalidate(callback) {
66
+ return this._knownToBeInvalid.onChange(() => callback());
67
+ }
68
+ /**
69
+ * Returns the access token if it is found in the cache, fetching it otherwise.
70
+ *
71
+ * This is usually the function you want to call to get an access token. When using the access token, you should catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).
72
+ *
73
+ * @returns null if the session is known to be invalid, cached tokens if they exist in the cache (which may or may not be valid still), or new tokens otherwise.
74
+ */
75
+ async getPotentiallyExpiredTokens() {
76
+ const accessToken = await this._getPotentiallyExpiredAccessToken();
77
+ return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
78
+ }
79
+ /**
80
+ * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
81
+ *
82
+ * 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.
83
+ *
84
+ * In most cases, you should prefer `getPotentiallyExpiredTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
85
+ *
86
+ * @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).
87
+ */
88
+ async fetchNewTokens() {
89
+ const accessToken = await this._getNewlyFetchedAccessToken();
90
+ return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
91
+ }
92
+ markAccessTokenExpired(accessToken) {
93
+ if (this._accessToken.get() === accessToken) {
94
+ this._accessToken.set(null);
95
+ }
96
+ }
97
+ /**
98
+ * Note that a callback invocation with `null` does not mean the session has been invalidated; the access token may just have expired. Use `onInvalidate` to detect invalidation.
99
+ */
100
+ onAccessTokenChange(callback) {
101
+ return this._accessToken.onChange(callback);
102
+ }
103
+ /**
104
+ * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
105
+ */
106
+ async _getPotentiallyExpiredAccessToken() {
107
+ const oldAccessToken = this._accessToken.get();
108
+ if (oldAccessToken)
109
+ return oldAccessToken;
110
+ if (!this._refreshToken)
111
+ return null;
112
+ if (this._knownToBeInvalid.get())
113
+ return null;
114
+ // refresh access token
115
+ if (!this._refreshPromise) {
116
+ this._refreshAndSetRefreshPromise(this._refreshToken);
117
+ }
118
+ return await this._refreshPromise;
119
+ }
120
+ /**
121
+ * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
122
+ *
123
+ * @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.
124
+ */
125
+ async _getNewlyFetchedAccessToken() {
126
+ if (!this._refreshToken)
127
+ return null;
128
+ if (this._knownToBeInvalid.get())
129
+ return null;
130
+ this._refreshAndSetRefreshPromise(this._refreshToken);
131
+ return await this._refreshPromise;
132
+ }
133
+ _refreshAndSetRefreshPromise(refreshToken) {
134
+ let refreshPromise = this._options.refreshAccessTokenCallback(refreshToken).then((accessToken) => {
135
+ if (refreshPromise === this._refreshPromise) {
136
+ this._refreshPromise = null;
137
+ this._accessToken.set(accessToken);
138
+ if (!accessToken) {
139
+ this.markInvalid();
140
+ }
141
+ }
142
+ return accessToken;
143
+ });
144
+ this._refreshPromise = refreshPromise;
145
+ }
146
+ }
@@ -1,4 +1,5 @@
1
1
  import { encodeBase32 } from "./bytes";
2
+ import { globalVar } from "./globals";
2
3
  /**
3
4
  * Generates a secure alphanumeric string using the system's cryptographically secure
4
5
  * random number generator.
@@ -6,7 +7,7 @@ import { encodeBase32 } from "./bytes";
6
7
  export function generateSecureRandomString(minBitsOfEntropy = 224) {
7
8
  const base32CharactersCount = Math.ceil(minBitsOfEntropy / 5);
8
9
  const bytesCount = Math.ceil(base32CharactersCount * 5 / 8);
9
- const randomBytes = globalThis.crypto.getRandomValues(new Uint8Array(bytesCount));
10
+ const randomBytes = globalVar.crypto.getRandomValues(new Uint8Array(bytesCount));
10
11
  const str = encodeBase32(randomBytes);
11
12
  return str.slice(str.length - base32CharactersCount).toLowerCase();
12
13
  }
package/dist/utils/dom.js CHANGED
@@ -4,5 +4,5 @@ export function hasClickableParent(element) {
4
4
  return false;
5
5
  if (parent.dataset.n2Clickable)
6
6
  return true;
7
- return !!element.parentElement && hasClickableParent(element.parentElement);
7
+ return hasClickableParent(element.parentElement);
8
8
  }
@@ -1,3 +1,4 @@
1
+ export declare function isBrowserLike(): boolean;
1
2
  /**
2
3
  * Returns the environment variable with the given name, throwing an error if it's undefined or the empty string.
3
4
  */
package/dist/utils/env.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { throwErr } from "./errors";
2
2
  import { deindent } from "./strings";
3
+ export function isBrowserLike() {
4
+ return typeof window !== "undefined" && typeof document !== "undefined" && typeof document.createElement !== "undefined";
5
+ }
3
6
  /**
4
7
  * Returns the environment variable with the given name, throwing an error if it's undefined or the empty string.
5
8
  */
6
9
  export function getEnvVariable(name) {
7
- if (typeof window !== 'undefined') {
10
+ if (isBrowserLike()) {
8
11
  throw new Error(deindent `
9
12
  Can't use getEnvVariable on the client because Next.js transpiles expressions of the kind process.env.XYZ at build-time on the client.
10
13
 
@@ -1,3 +1,4 @@
1
+ import { globalVar } from "./globals";
1
2
  export function throwErr(...args) {
2
3
  if (typeof args[0] === "string") {
3
4
  throw new StackAssertionError(args[0], args[1]);
@@ -29,6 +30,10 @@ export function registerErrorSink(sink) {
29
30
  registerErrorSink((location, ...args) => {
30
31
  console.error(`Error in ${location}:`, ...args);
31
32
  });
33
+ registerErrorSink((location, error, ...args) => {
34
+ globalVar.stackCapturedErrors = globalVar.stackCapturedErrors ?? [];
35
+ globalVar.stackCapturedErrors.push({ location, error: args, extraArgs: args });
36
+ });
32
37
  export function captureError(location, error) {
33
38
  for (const sink of errorSinks) {
34
39
  sink(location, error);
@@ -1,6 +1,6 @@
1
1
  const globalVar = typeof globalThis !== 'undefined' ? globalThis :
2
- typeof window !== 'undefined' ? window :
3
- typeof global !== 'undefined' ? global :
2
+ typeof global !== 'undefined' ? global :
3
+ typeof window !== 'undefined' ? window :
4
4
  typeof self !== 'undefined' ? self :
5
5
  {};
6
6
  export { globalVar, };
@@ -18,8 +18,10 @@ export declare function pending<T>(promise: Promise<T>, options?: {
18
18
  }): ReactPromise<T>;
19
19
  export declare function wait(ms: number): Promise<void>;
20
20
  export declare function waitUntil(date: Date): Promise<void>;
21
+ export declare function runAsynchronouslyWithAlert(...args: Parameters<typeof runAsynchronously>): void;
21
22
  export declare function runAsynchronously(promiseOrFunc: void | Promise<unknown> | (() => void | Promise<unknown>) | undefined, options?: {
22
- ignoreErrors?: boolean;
23
+ noErrorLogging?: boolean;
24
+ onError?: (error: Error) => void;
23
25
  }): void;
24
26
  declare class TimeoutError extends Error {
25
27
  readonly ms: number;
@@ -75,6 +75,15 @@ class ErrorDuringRunAsynchronously extends Error {
75
75
  this.name = "ErrorDuringRunAsynchronously";
76
76
  }
77
77
  }
78
+ export function runAsynchronouslyWithAlert(...args) {
79
+ return runAsynchronously(args[0], {
80
+ ...args[1],
81
+ onError: error => {
82
+ alert(`An unhandled error occurred. Please ${process.env.NODE_ENV === "development" ? `check the browser console for the full error. ${error}` : "report this to the developer."}`);
83
+ args[1]?.onError?.(error);
84
+ },
85
+ }, ...args.slice(2));
86
+ }
78
87
  export function runAsynchronously(promiseOrFunc, options = {}) {
79
88
  if (typeof promiseOrFunc === "function") {
80
89
  promiseOrFunc = promiseOrFunc();
@@ -86,7 +95,8 @@ export function runAsynchronously(promiseOrFunc, options = {}) {
86
95
  }, {
87
96
  cause: error,
88
97
  });
89
- if (!options.ignoreErrors) {
98
+ options.onError?.(newError);
99
+ if (!options.noErrorLogging) {
90
100
  captureError("runAsynchronously", newError);
91
101
  }
92
102
  });
@@ -0,0 +1 @@
1
+ export declare function logged<T extends object>(name: string, toLog: T, options?: {}): T;
@@ -0,0 +1,59 @@
1
+ import { nicify } from "./strings";
2
+ export function logged(name, toLog, options = {}) {
3
+ const proxy = new Proxy(toLog, {
4
+ get(target, prop, receiver) {
5
+ const orig = Reflect.get(target, prop, receiver);
6
+ if (typeof orig === "function") {
7
+ return function (...args) {
8
+ const success = (v, isPromise) => console.debug(`logged(...): Called ${name}.${String(prop)}(${args.map(a => nicify(a)).join(", ")}) => ${isPromise ? "Promise<" : ""}${nicify(result)}${isPromise ? ">" : ""}`, { this: this, args, promise: isPromise ? result : false, result: v, trace: new Error() });
9
+ const error = (e, isPromise) => console.debug(`logged(...): Error in ${name}.${String(prop)}(${args.map(a => nicify(a)).join(", ")})`, { this: this, args, promise: isPromise ? result : false, error: e, trace: new Error() });
10
+ let result;
11
+ try {
12
+ result = orig.apply(this, args);
13
+ }
14
+ catch (e) {
15
+ error(e, false);
16
+ throw e;
17
+ }
18
+ if (result instanceof Promise) {
19
+ result.then((v) => success(v, true)).catch((e) => error(e, true));
20
+ }
21
+ else {
22
+ success(result, false);
23
+ }
24
+ return result;
25
+ };
26
+ }
27
+ return orig;
28
+ },
29
+ set(target, prop, value) {
30
+ console.log(`Setting ${name}.${String(prop)} to ${value}`);
31
+ return Reflect.set(target, prop, value);
32
+ },
33
+ apply(target, thisArg, args) {
34
+ console.log(`Calling ${name}(${JSON.stringify(args).slice(1, -1)})`);
35
+ return Reflect.apply(target, thisArg, args);
36
+ },
37
+ construct(target, args, newTarget) {
38
+ console.log(`Constructing ${name}(${JSON.stringify(args).slice(1, -1)})`);
39
+ return Reflect.construct(target, args, newTarget);
40
+ },
41
+ defineProperty(target, prop, descriptor) {
42
+ console.log(`Defining ${name}.${String(prop)} as ${JSON.stringify(descriptor)}`);
43
+ return Reflect.defineProperty(target, prop, descriptor);
44
+ },
45
+ deleteProperty(target, prop) {
46
+ console.log(`Deleting ${name}.${String(prop)}`);
47
+ return Reflect.deleteProperty(target, prop);
48
+ },
49
+ setPrototypeOf(target, prototype) {
50
+ console.log(`Setting prototype of ${name} to ${prototype}`);
51
+ return Reflect.setPrototypeOf(target, prototype);
52
+ },
53
+ preventExtensions(target) {
54
+ console.log(`Preventing extensions of ${name}`);
55
+ return Reflect.preventExtensions(target);
56
+ },
57
+ });
58
+ return proxy;
59
+ }
@@ -1,6 +1,7 @@
1
1
  import { use } from "react";
2
2
  import { neverResolve } from "./promises";
3
3
  import { deindent } from "./strings";
4
+ import { isBrowserLike } from "./env";
4
5
  export function getNodeText(node) {
5
6
  if (["number", "string"].includes(typeof node)) {
6
7
  return `${node}`;
@@ -29,7 +30,7 @@ export function suspend() {
29
30
  * Use this in a component or a hook to disable SSR. Should be wrapped in a Suspense boundary, or it will throw an error.
30
31
  */
31
32
  export function suspendIfSsr(caller) {
32
- if (typeof window === "undefined") {
33
+ if (!isBrowserLike()) {
33
34
  const error = Object.assign(new Error(deindent `
34
35
  ${caller ?? "This code path"} attempted to display a loading indicator during SSR by falling back to the nearest Suspense boundary. If you see this error, it means no Suspense boundary was found, and no loading indicator could be displayed. Make sure you are not catching this error with try-catch, and that the component is rendered inside a Suspense boundary, for example by adding a \`loading.tsx\` file in your app directory.
35
36
 
@@ -5,11 +5,11 @@ export type Result<T, E = unknown> = {
5
5
  status: "error";
6
6
  error: E;
7
7
  };
8
- export type AsyncResult<T, E = unknown, P = void> = Result<T, E> | {
8
+ export type AsyncResult<T, E = unknown, P = void> = Result<T, E> | ({
9
9
  status: "pending";
10
10
  } & {
11
11
  progress: P;
12
- };
12
+ });
13
13
  export declare const Result: {
14
14
  fromThrowing: typeof fromThrowing;
15
15
  fromPromise: typeof promiseToResult;
@@ -62,7 +62,7 @@ declare function promiseToResult<T>(promise: Promise<T>): Promise<Result<T>>;
62
62
  declare function fromThrowing<T>(fn: () => T): Result<T, unknown>;
63
63
  declare function mapResult<T, U, E = unknown, P = unknown>(result: Result<T, E>, fn: (data: T) => U): Result<U, E>;
64
64
  declare function mapResult<T, U, E = unknown, P = unknown>(result: AsyncResult<T, E, P>, fn: (data: T) => U): AsyncResult<U, E, P>;
65
- declare class RetryError extends Error {
65
+ declare class RetryError extends AggregateError {
66
66
  readonly errors: unknown[];
67
67
  constructor(errors: unknown[]);
68
68
  get retries(): number;
@@ -1,4 +1,5 @@
1
1
  import { wait } from "./promises";
2
+ import { deindent } from "./strings";
2
3
  export const Result = {
3
4
  fromThrowing,
4
5
  fromPromise: promiseToResult,
@@ -83,10 +84,17 @@ function mapResult(result, fn) {
83
84
  };
84
85
  return Result.ok(fn(result.data));
85
86
  }
86
- class RetryError extends Error {
87
+ class RetryError extends AggregateError {
87
88
  errors;
88
89
  constructor(errors) {
89
- super(`Error after retrying ${errors.length} times.`, { cause: errors[errors.length - 1] });
90
+ super(errors, deindent `
91
+ Error after retrying ${errors.length} times.
92
+
93
+ ${errors.map((e, i) => deindent `
94
+ Attempt ${i + 1}:
95
+ ${e}
96
+ `).join("\n\n")}
97
+ `, { cause: errors[errors.length - 1] });
90
98
  this.errors = errors;
91
99
  this.name = "RetryError";
92
100
  }
@@ -1,5 +1,28 @@
1
1
  import { AsyncResult, Result } from "./results";
2
2
  import { ReactPromise } from "./promises";
3
+ export type ReadonlyStore<T> = {
4
+ get(): T;
5
+ onChange(callback: (value: T, oldValue: T | undefined) => void): {
6
+ unsubscribe: () => void;
7
+ };
8
+ onceChange(callback: (value: T, oldValue: T | undefined) => void): {
9
+ unsubscribe: () => void;
10
+ };
11
+ };
12
+ export declare class Store<T> implements ReadonlyStore<T> {
13
+ private _value;
14
+ private readonly _callbacks;
15
+ constructor(_value: T);
16
+ get(): T;
17
+ set(value: T): void;
18
+ update(updater: (value: T) => T): T;
19
+ onChange(callback: (value: T, oldValue: T | undefined) => void): {
20
+ unsubscribe: () => void;
21
+ };
22
+ onceChange(callback: (value: T, oldValue: T | undefined) => void): {
23
+ unsubscribe: () => void;
24
+ };
25
+ }
3
26
  export type ReadonlyAsyncStore<T> = {
4
27
  isAvailable(): boolean;
5
28
  get(): AsyncResult<T, unknown, void>;
@@ -1,6 +1,42 @@
1
1
  import { AsyncResult, Result } from "./results";
2
2
  import { generateUuid } from "./uuids";
3
3
  import { pending, rejected, resolved } from "./promises";
4
+ export class Store {
5
+ _value;
6
+ _callbacks = new Map();
7
+ constructor(_value) {
8
+ this._value = _value;
9
+ }
10
+ get() {
11
+ return this._value;
12
+ }
13
+ set(value) {
14
+ const oldValue = this._value;
15
+ this._value = value;
16
+ this._callbacks.forEach((callback) => callback(value, oldValue));
17
+ }
18
+ update(updater) {
19
+ const value = updater(this._value);
20
+ this.set(value);
21
+ return value;
22
+ }
23
+ onChange(callback) {
24
+ const uuid = generateUuid();
25
+ this._callbacks.set(uuid, callback);
26
+ return {
27
+ unsubscribe: () => {
28
+ this._callbacks.delete(uuid);
29
+ },
30
+ };
31
+ }
32
+ onceChange(callback) {
33
+ const { unsubscribe } = this.onChange((...args) => {
34
+ unsubscribe();
35
+ callback(...args);
36
+ });
37
+ return { unsubscribe };
38
+ }
39
+ }
4
40
  export class AsyncStore {
5
41
  _isAvailable;
6
42
  _value = undefined;
@@ -39,3 +39,6 @@ export declare function trimLines(s: string): string;
39
39
  export declare function templateIdentity(strings: TemplateStringsArray | readonly string[], ...values: any[]): string;
40
40
  export declare function deindent(code: string): string;
41
41
  export declare function deindent(strings: TemplateStringsArray | readonly string[], ...values: any[]): string;
42
+ export declare function nicify(value: unknown, { depth }?: {
43
+ depth?: number | undefined;
44
+ }): string;
@@ -92,3 +92,49 @@ export function deindent(strings, ...values) {
92
92
  });
93
93
  return templateIdentity(deindentedStrings, ...indentedValues);
94
94
  }
95
+ export function nicify(value, { depth = 5 } = {}) {
96
+ switch (typeof value) {
97
+ case "string":
98
+ case "boolean":
99
+ case "number": {
100
+ return JSON.stringify(value);
101
+ }
102
+ case "undefined": {
103
+ return "undefined";
104
+ }
105
+ case "symbol": {
106
+ return value.toString();
107
+ }
108
+ case "bigint": {
109
+ return `${value}n`;
110
+ }
111
+ case "function": {
112
+ if (value.name)
113
+ return `function ${value.name}(...) { ... }`;
114
+ return `(...) => { ... }`;
115
+ }
116
+ case "object": {
117
+ if (value === null)
118
+ return "null";
119
+ if (Array.isArray(value)) {
120
+ if (depth <= 0 && value.length !== 0)
121
+ return "[...]";
122
+ return `[${value.map((v) => nicify(v, { depth: depth - 1 })).join(", ")}]`;
123
+ }
124
+ const entries = Object.entries(value);
125
+ if (entries.length === 0)
126
+ return "{}";
127
+ if (depth <= 0)
128
+ return "{...}";
129
+ return `{ ${Object.entries(value).map(([k, v]) => {
130
+ if (typeof v === "function" && v.name === k)
131
+ return `${k}(...): { ... }`;
132
+ else
133
+ return `${k}: ${nicify(v, { depth: depth - 1 })}`;
134
+ }).join(", ")} }`;
135
+ }
136
+ default: {
137
+ return `${typeof value}<${value}>`;
138
+ }
139
+ }
140
+ }
@@ -1 +1 @@
1
- export declare function generateUuid(): string;
1
+ export declare function generateUuid(): any;
@@ -1,3 +1,4 @@
1
+ import { globalVar } from "./globals";
1
2
  export function generateUuid() {
2
- return globalThis.crypto.randomUUID();
3
+ return globalVar.crypto.randomUUID();
3
4
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.4.13",
3
+ "version": "2.4.20",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -26,7 +26,7 @@
26
26
  "jose": "^5.2.2",
27
27
  "oauth4webapi": "^2.10.3",
28
28
  "uuid": "^9.0.1",
29
- "@stackframe/stack-sc": "1.5.6"
29
+ "@stackframe/stack-sc": "2.4.20"
30
30
  },
31
31
  "devDependencies": {
32
32
  "rimraf": "^5.0.5",