@stackframe/stack-shared 2.4.13 → 2.4.14

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,127 @@
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
+ * A session 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 may change over time.
18
+ */
19
+ export class Session {
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 makes session keys 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 = Session.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
+ invalidate() {
59
+ this._accessToken.set(null);
60
+ this._knownToBeInvalid.set(true);
61
+ }
62
+ onInvalidate(callback) {
63
+ return this._knownToBeInvalid.onChange(() => callback());
64
+ }
65
+ async getPotentiallyExpiredTokens() {
66
+ const accessToken = await this._getPotentiallyExpiredAccessToken();
67
+ return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
68
+ }
69
+ async getNewlyFetchedTokens() {
70
+ const accessToken = await this._getNewlyFetchedAccessToken();
71
+ return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
72
+ }
73
+ markAccessTokenExpired(accessToken) {
74
+ if (this._accessToken.get() === accessToken) {
75
+ this._accessToken.set(null);
76
+ }
77
+ }
78
+ /**
79
+ * 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.
80
+ */
81
+ onAccessTokenChange(callback) {
82
+ return this._accessToken.onChange(callback);
83
+ }
84
+ /**
85
+ * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
86
+ */
87
+ async _getPotentiallyExpiredAccessToken() {
88
+ const oldAccessToken = this._accessToken.get();
89
+ if (oldAccessToken)
90
+ return oldAccessToken;
91
+ if (!this._refreshToken)
92
+ return null;
93
+ if (this._knownToBeInvalid.get())
94
+ return null;
95
+ // refresh access token
96
+ if (!this._refreshPromise) {
97
+ this._refreshAndSetRefreshPromise(this._refreshToken);
98
+ }
99
+ return await this._refreshPromise;
100
+ }
101
+ /**
102
+ * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
103
+ *
104
+ * @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
+ */
106
+ async _getNewlyFetchedAccessToken() {
107
+ if (!this._refreshToken)
108
+ return null;
109
+ if (this._knownToBeInvalid.get())
110
+ return null;
111
+ this._refreshAndSetRefreshPromise(this._refreshToken);
112
+ return await this._refreshPromise;
113
+ }
114
+ _refreshAndSetRefreshPromise(refreshToken) {
115
+ let refreshPromise = this._options.refreshAccessTokenCallback(refreshToken).then((accessToken) => {
116
+ if (refreshPromise === this._refreshPromise) {
117
+ this._refreshPromise = null;
118
+ this._accessToken.set(accessToken);
119
+ if (!accessToken) {
120
+ this.invalidate();
121
+ }
122
+ }
123
+ return accessToken;
124
+ });
125
+ this._refreshPromise = refreshPromise;
126
+ }
127
+ }
@@ -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.14",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [