@stackframe/stack-shared 2.5.16 → 2.5.18

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.
@@ -1,4 +1,6 @@
1
1
  import * as yup from "yup";
2
+ import { isBase64 } from "./utils/bytes";
3
+ import { StackAssertionError } from "./utils/errors";
2
4
  import { allProviders } from "./utils/oauth";
3
5
  import { isUuid } from "./utils/uuids";
4
6
  const _idDescription = (identify) => `The unique identifier of this ${identify}`;
@@ -55,6 +57,30 @@ export function yupObject(...args) {
55
57
  return object.default(undefined);
56
58
  }
57
59
  /* eslint-enable no-restricted-syntax */
60
+ export function yupUnion(...args) {
61
+ if (args.length === 0)
62
+ throw new Error('yupUnion must have at least one schema');
63
+ const [first] = args;
64
+ const firstDesc = first.describe();
65
+ for (const schema of args) {
66
+ const desc = schema.describe();
67
+ if (desc.type !== firstDesc.type)
68
+ throw new StackAssertionError(`yupUnion must have schemas of the same type (got: ${firstDesc.type} and ${desc.type})`, { first, schema, firstDesc, desc });
69
+ }
70
+ return yupMixed().required().test('is-one-of', 'Invalid value', async (value, context) => {
71
+ const errors = [];
72
+ for (const schema of args) {
73
+ try {
74
+ await schema.validate(value, context.options);
75
+ return true;
76
+ }
77
+ catch (e) {
78
+ errors.push(e);
79
+ }
80
+ }
81
+ throw new AggregateError(errors, 'Invalid value; must be one of the provided schemas');
82
+ });
83
+ }
58
84
  // Common
59
85
  export const adaptSchema = yupMixed();
60
86
  /**
@@ -99,6 +125,11 @@ export const jsonStringOrEmptySchema = yupString().test("json", "Invalid JSON fo
99
125
  }
100
126
  });
101
127
  export const emailSchema = yupString().email();
128
+ export const base64Schema = yupString().test("is-base64", "Invalid base64 format", (value) => {
129
+ if (value == null)
130
+ return true;
131
+ return isBase64(value);
132
+ });
102
133
  // Request auth
103
134
  export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']);
104
135
  export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']);
@@ -115,6 +146,7 @@ export const projectConfigIdSchema = yupString().meta({ openapiField: { descript
115
146
  export const projectAllowLocalhostSchema = yupBoolean().meta({ openapiField: { description: 'Whether localhost is allowed as a domain for this project. Should only be allowed in development mode', exampleValue: true } });
116
147
  export const projectCreateTeamOnSignUpSchema = yupBoolean().meta({ openapiField: { description: 'Whether a team should be created for each user that signs up', exampleValue: true } });
117
148
  export const projectMagicLinkEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether magic link authentication is enabled for this project', exampleValue: true } });
149
+ export const projectSignUpEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether users can sign up new accounts, or whether they are only allowed to sign in to existing accounts. Regardless of this option, the server API can always create new users with the `POST /users` endpoint.', exampleValue: true } });
118
150
  export const projectCredentialEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether email password authentication is enabled for this project', exampleValue: true } });
119
151
  // Project OAuth config
120
152
  export const oauthIdSchema = yupString().oneOf(allProviders).meta({ openapiField: { description: `OAuth provider ID, one of ${allProviders.map(x => `\`${x}\``).join(', ')}`, exampleValue: 'google' } });
@@ -155,11 +187,15 @@ export const userIdSchema = yupString().uuid().meta({ openapiField: { descriptio
155
187
  export const primaryEmailSchema = emailSchema.meta({ openapiField: { description: 'Primary email', exampleValue: 'johndoe@example.com' } });
156
188
  export const primaryEmailVerifiedSchema = yupBoolean().meta({ openapiField: { description: 'Whether the primary email has been verified to belong to this user', exampleValue: true } });
157
189
  export const userDisplayNameSchema = yupString().nullable().meta({ openapiField: { description: _displayNameDescription('user'), exampleValue: 'John Doe' } });
158
- export const selectedTeamIdSchema = yupString().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
190
+ export const selectedTeamIdSchema = yupString().uuid().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
159
191
  export const profileImageUrlSchema = yupString().meta({ openapiField: { description: _profileImageUrlDescription('user'), exampleValue: 'https://example.com/image.jpg' } });
160
192
  export const signedUpAtMillisSchema = yupNumber().meta({ openapiField: { description: _signedUpAtMillisDescription, exampleValue: 1630000000000 } });
161
193
  export const userClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('user'), exampleValue: { key: 'value' } } });
162
194
  export const userServerMetadataSchema = jsonSchema.meta({ openapiField: { description: _serverMetaDataDescription('user'), exampleValue: { key: 'value' } } });
195
+ export const userOAuthProviderSchema = yupObject({
196
+ type: yupString().required(),
197
+ provider_user_id: yupString().required(),
198
+ });
163
199
  // Auth
164
200
  export const signInEmailSchema = emailSchema.meta({ openapiField: { description: 'The email to sign in with.', exampleValue: 'johndoe@example.com' } });
165
201
  export const emailOtpSignInCallbackUrlSchema = urlSchema.meta({ openapiField: { description: 'The base callback URL to construct the magic link from. A query argument `code` with the verification code will be appended to it. The page should then make a request to the `/auth/otp/sign-in` endpoint.', exampleValue: 'https://example.com/handler/magic-link-callback' } });
@@ -203,6 +239,8 @@ export const teamProfileImageUrlSchema = yupString().meta({ openapiField: { desc
203
239
  export const teamClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('team'), exampleValue: { key: 'value' } } });
204
240
  export const teamServerMetadataSchema = jsonSchema.meta({ openapiField: { description: _serverMetaDataDescription('team'), exampleValue: { key: 'value' } } });
205
241
  export const teamCreatedAtMillisSchema = yupNumber().meta({ openapiField: { description: _createdAtMillisDescription('team'), exampleValue: 1630000000000 } });
242
+ export const teamInvitationEmailSchema = emailSchema.meta({ openapiField: { description: 'The email to sign in with.', exampleValue: 'johndoe@example.com' } });
243
+ export const teamInvitationCallbackUrlSchema = urlSchema.meta({ openapiField: { description: 'The base callback URL to construct a verification link for the verification e-mail. A query argument `code` with the verification code will be appended to it. The page should then make a request to the `/contact-channels/verify` endpoint.', exampleValue: 'https://example.com/handler/email-verification' } });
206
244
  // Team member profiles
207
245
  export const teamMemberDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('team member') + ' Note that this is separate from the display_name of the user.', exampleValue: 'John Doe' } });
208
246
  export const teamMemberProfileImageUrlSchema = yupString().meta({ openapiField: { description: _profileImageUrlDescription('team member'), exampleValue: 'https://example.com/image.jpg' } });
@@ -13,3 +13,4 @@ export declare function rotateLeft(arr: readonly any[], n: number): any[];
13
13
  export declare function rotateRight(arr: readonly any[], n: number): any[];
14
14
  export declare function shuffle<T>(arr: readonly T[]): T[];
15
15
  export declare function outerProduct<T, U>(arr1: readonly T[], arr2: readonly U[]): [T, U][];
16
+ export declare function unique<T>(arr: readonly T[]): T[];
@@ -65,3 +65,6 @@ export function shuffle(arr) {
65
65
  export function outerProduct(arr1, arr2) {
66
66
  return arr1.flatMap((item1) => arr2.map((item2) => [item1, item2]));
67
67
  }
68
+ export function unique(arr) {
69
+ return [...new Set(arr)];
70
+ }
@@ -1,2 +1,6 @@
1
1
  export declare function encodeBase32(input: Uint8Array): string;
2
2
  export declare function decodeBase32(input: string): Uint8Array;
3
+ export declare function encodeBase64(input: Uint8Array): string;
4
+ export declare function decodeBase64(input: string): Uint8Array;
5
+ export declare function isBase32(input: string): boolean;
6
+ export declare function isBase64(input: string): boolean;
@@ -1,3 +1,4 @@
1
+ import { StackAssertionError } from "./errors";
1
2
  const crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2
3
  const crockfordReplacements = new Map([
3
4
  ["o", "0"],
@@ -19,9 +20,16 @@ export function encodeBase32(input) {
19
20
  if (bits > 0) {
20
21
  output += crockfordAlphabet[(value << (5 - bits)) & 31];
21
22
  }
23
+ // sanity check
24
+ if (!isBase32(output)) {
25
+ throw new StackAssertionError("Invalid base32 output; this should never happen");
26
+ }
22
27
  return output;
23
28
  }
24
29
  export function decodeBase32(input) {
30
+ if (!isBase32(input)) {
31
+ throw new StackAssertionError("Invalid base32 string");
32
+ }
25
33
  const output = new Uint8Array((input.length * 5 / 8) | 0);
26
34
  let bits = 0;
27
35
  let value = 0;
@@ -46,3 +54,31 @@ export function decodeBase32(input) {
46
54
  }
47
55
  return output;
48
56
  }
57
+ export function encodeBase64(input) {
58
+ const res = btoa(String.fromCharCode(...input));
59
+ // sanity check
60
+ if (!isBase64(res)) {
61
+ throw new StackAssertionError("Invalid base64 output; this should never happen");
62
+ }
63
+ return res;
64
+ }
65
+ export function decodeBase64(input) {
66
+ if (!isBase64(input)) {
67
+ throw new StackAssertionError("Invalid base64 string");
68
+ }
69
+ return new Uint8Array(atob(input).split("").map((char) => char.charCodeAt(0)));
70
+ }
71
+ export function isBase32(input) {
72
+ for (const char of input) {
73
+ if (char === " ")
74
+ continue;
75
+ if (!crockfordAlphabet.includes(char)) {
76
+ return false;
77
+ }
78
+ }
79
+ return true;
80
+ }
81
+ export function isBase64(input) {
82
+ const regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
83
+ return regex.test(input);
84
+ }
@@ -1,3 +1,4 @@
1
+ export declare function generateRandomValues(array: Uint8Array): typeof array;
1
2
  /**
2
3
  * Generates a secure alphanumeric string using the system's cryptographically secure
3
4
  * random number generator.
@@ -1,5 +1,15 @@
1
1
  import { encodeBase32 } from "./bytes";
2
+ import { StackAssertionError } from "./errors";
2
3
  import { globalVar } from "./globals";
4
+ export function generateRandomValues(array) {
5
+ if (!globalVar.crypto) {
6
+ throw new StackAssertionError("Crypto API is not available in this environment. Are you using an old browser?");
7
+ }
8
+ if (!globalVar.crypto.getRandomValues) {
9
+ throw new StackAssertionError("crypto.getRandomValues is not available in this environment. Are you using an old browser?");
10
+ }
11
+ return globalVar.crypto.getRandomValues(array);
12
+ }
3
13
  /**
4
14
  * Generates a secure alphanumeric string using the system's cryptographically secure
5
15
  * random number generator.
@@ -7,7 +17,7 @@ import { globalVar } from "./globals";
7
17
  export function generateSecureRandomString(minBitsOfEntropy = 224) {
8
18
  const base32CharactersCount = Math.ceil(minBitsOfEntropy / 5);
9
19
  const bytesCount = Math.ceil(base32CharactersCount * 5 / 8);
10
- const randomBytes = globalVar.crypto.getRandomValues(new Uint8Array(bytesCount));
20
+ const randomBytes = generateRandomValues(new Uint8Array(bytesCount));
11
21
  const str = encodeBase32(randomBytes);
12
22
  return str.slice(str.length - base32CharactersCount).toLowerCase();
13
23
  }
@@ -12,6 +12,7 @@ export type AsyncResult<T, E = unknown, P = void> = Result<T, E> | ({
12
12
  });
13
13
  export declare const Result: {
14
14
  fromThrowing: typeof fromThrowing;
15
+ fromThrowingAsync: typeof fromThrowingAsync;
15
16
  fromPromise: typeof promiseToResult;
16
17
  ok<T>(data: T): {
17
18
  status: "ok";
@@ -60,6 +61,7 @@ declare function pending<P>(progress: P): AsyncResult<never, never, P> & {
60
61
  };
61
62
  declare function promiseToResult<T>(promise: Promise<T>): Promise<Result<T>>;
62
63
  declare function fromThrowing<T>(fn: () => T): Result<T, unknown>;
64
+ declare function fromThrowingAsync<T>(fn: () => Promise<T>): Promise<Result<T, unknown>>;
63
65
  declare function mapResult<T, U, E = unknown, P = unknown>(result: Result<T, E>, fn: (data: T) => U): Result<U, E>;
64
66
  declare function mapResult<T, U, E = unknown, P = unknown>(result: AsyncResult<T, E, P>, fn: (data: T) => U): AsyncResult<U, E, P>;
65
67
  declare class RetryError extends AggregateError {
@@ -2,6 +2,7 @@ import { wait } from "./promises";
2
2
  import { deindent } from "./strings";
3
3
  export const Result = {
4
4
  fromThrowing,
5
+ fromThrowingAsync,
5
6
  fromPromise: promiseToResult,
6
7
  ok(data) {
7
8
  return {
@@ -71,6 +72,14 @@ function fromThrowing(fn) {
71
72
  return Result.error(error);
72
73
  }
73
74
  }
75
+ async function fromThrowingAsync(fn) {
76
+ try {
77
+ return Result.ok(await fn());
78
+ }
79
+ catch (error) {
80
+ return Result.error(error);
81
+ }
82
+ }
74
83
  function mapResult(result, fn) {
75
84
  if (result.status === "error")
76
85
  return {
@@ -1,4 +1,4 @@
1
- import { findLastIndex } from "./arrays";
1
+ import { findLastIndex, unique } from "./arrays";
2
2
  import { StackAssertionError } from "./errors";
3
3
  import { filterUndefined } from "./objects";
4
4
  export function typedToLowercase(s) {
@@ -207,6 +207,9 @@ export function nicify(value, options = {}) {
207
207
  return `[${resValues.join(", ")}]`;
208
208
  }
209
209
  }
210
+ if (value instanceof URL) {
211
+ return `URL(${JSON.stringify(value.toString())})`;
212
+ }
210
213
  const constructorName = [null, Object.prototype].includes(Object.getPrototypeOf(value)) ? null : (nicifiableClassNameOverrides.get(value.constructor) ?? value.constructor.name);
211
214
  const constructorString = constructorName ? `${nicifyPropertyString(constructorName)} ` : "";
212
215
  const entries = getNicifiableEntries(value).filter(([k]) => !hideFields.includes(k));
@@ -256,7 +259,16 @@ function nicifyPropertyString(str) {
256
259
  return JSON.stringify(str);
257
260
  }
258
261
  function getNicifiableKeys(value) {
259
- return ("getNicifiableKeys" in value ? value.getNicifiableKeys?.bind(value) : null)?.() ?? Object.keys(value).sort();
262
+ const overridden = ("getNicifiableKeys" in value ? value.getNicifiableKeys?.bind(value) : null)?.();
263
+ if (overridden != null)
264
+ return overridden;
265
+ const keys = Object.keys(value).sort();
266
+ if (value instanceof Error) {
267
+ if (value.cause)
268
+ keys.unshift("cause");
269
+ keys.unshift("message", "stack");
270
+ }
271
+ return unique(keys);
260
272
  }
261
273
  function getNicifiableEntries(value) {
262
274
  const recordLikes = [Headers];
@@ -1,7 +1,7 @@
1
- import { globalVar } from "./globals";
1
+ import { generateRandomValues } from "./crypto";
2
2
  export function generateUuid() {
3
3
  // crypto.randomUuid is not supported in all browsers, so this is a polyfill
4
- return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => (+c ^ globalVar.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));
4
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => (+c ^ generateRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16));
5
5
  }
6
6
  export function isUuid(str) {
7
7
  return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(str);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.5.16",
3
+ "version": "2.5.18",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -36,7 +36,7 @@
36
36
  "jose": "^5.2.2",
37
37
  "oauth4webapi": "^2.10.3",
38
38
  "uuid": "^9.0.1",
39
- "@stackframe/stack-sc": "2.5.16"
39
+ "@stackframe/stack-sc": "2.5.18"
40
40
  },
41
41
  "devDependencies": {
42
42
  "rimraf": "^5.0.5",