@stackframe/stack-shared 2.5.3 → 2.5.5

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/crud.d.ts +10 -3
  3. package/dist/helpers/production-mode.d.ts +6 -0
  4. package/dist/helpers/production-mode.js +43 -0
  5. package/dist/index.d.ts +4 -4
  6. package/dist/index.js +4 -4
  7. package/dist/interface/adminInterface.d.ts +28 -67
  8. package/dist/interface/adminInterface.js +63 -21
  9. package/dist/interface/clientInterface.d.ts +21 -133
  10. package/dist/interface/clientInterface.js +92 -118
  11. package/dist/interface/crud/api-keys.d.ts +134 -0
  12. package/dist/interface/crud/api-keys.js +61 -0
  13. package/dist/interface/crud/current-user.d.ts +47 -11
  14. package/dist/interface/crud/current-user.js +7 -3
  15. package/dist/interface/crud/email-templates.d.ts +53 -34
  16. package/dist/interface/crud/email-templates.js +37 -24
  17. package/dist/interface/crud/oauth.d.ts +8 -9
  18. package/dist/interface/crud/oauth.js +5 -5
  19. package/dist/interface/crud/projects.d.ts +458 -0
  20. package/dist/interface/crud/projects.js +112 -0
  21. package/dist/interface/crud/team-memberships.d.ts +22 -0
  22. package/dist/interface/crud/team-memberships.js +22 -0
  23. package/dist/interface/crud/team-permissions.d.ts +129 -0
  24. package/dist/interface/crud/team-permissions.js +83 -0
  25. package/dist/interface/crud/teams.d.ts +148 -0
  26. package/dist/interface/crud/teams.js +80 -0
  27. package/dist/interface/crud/users.d.ts +88 -33
  28. package/dist/interface/crud/users.js +22 -14
  29. package/dist/interface/crud-deprecated/api-keys.d.ts +134 -0
  30. package/dist/interface/crud-deprecated/api-keys.js +61 -0
  31. package/dist/interface/crud-deprecated/current-user.d.ts +127 -0
  32. package/dist/interface/crud-deprecated/current-user.js +49 -0
  33. package/dist/interface/crud-deprecated/email-templates.d.ts +75 -0
  34. package/dist/interface/crud-deprecated/email-templates.js +41 -0
  35. package/dist/interface/crud-deprecated/oauth.d.ts +24 -0
  36. package/dist/interface/crud-deprecated/oauth.js +12 -0
  37. package/dist/interface/crud-deprecated/projects.d.ts +440 -0
  38. package/dist/interface/crud-deprecated/projects.js +109 -0
  39. package/dist/interface/crud-deprecated/team-memberships.d.ts +22 -0
  40. package/dist/interface/crud-deprecated/team-memberships.js +22 -0
  41. package/dist/interface/crud-deprecated/team-permissions.d.ts +129 -0
  42. package/dist/interface/crud-deprecated/team-permissions.js +83 -0
  43. package/dist/interface/crud-deprecated/teams.d.ts +126 -0
  44. package/dist/interface/crud-deprecated/teams.js +78 -0
  45. package/dist/interface/crud-deprecated/users.d.ts +201 -0
  46. package/dist/interface/crud-deprecated/users.js +75 -0
  47. package/dist/interface/serverInterface.d.ts +33 -60
  48. package/dist/interface/serverInterface.js +74 -101
  49. package/dist/known-errors.d.ts +43 -26
  50. package/dist/known-errors.js +132 -85
  51. package/dist/schema-fields.d.ts +53 -4
  52. package/dist/schema-fields.js +156 -25
  53. package/dist/sessions.d.ts +1 -0
  54. package/dist/sessions.js +13 -3
  55. package/dist/utils/compile-time.d.ts +3 -1
  56. package/dist/utils/compile-time.js +3 -1
  57. package/dist/utils/errors.d.ts +8 -1
  58. package/dist/utils/errors.js +17 -4
  59. package/dist/utils/objects.d.ts +4 -1
  60. package/dist/utils/objects.js +16 -8
  61. package/dist/utils/promises.js +6 -1
  62. package/dist/utils/proxies.d.ts +1 -0
  63. package/dist/utils/proxies.js +65 -0
  64. package/dist/utils/react.d.ts +1 -1
  65. package/dist/utils/react.js +2 -2
  66. package/dist/utils/strings.js +3 -3
  67. package/dist/utils/urls.d.ts +1 -0
  68. package/dist/utils/urls.js +8 -0
  69. package/package.json +2 -2
  70. package/dist/utils/yup.d.ts +0 -3
  71. package/dist/utils/yup.js +0 -13
@@ -1,11 +1,63 @@
1
1
  import * as yup from "yup";
2
- import { yupJson } from "./utils/yup";
2
+ const _idDescription = (identify) => `The immutable ID used to uniquely identify this ${identify}`;
3
+ const _displayNameDescription = (identify) => `Human-readable ${identify} display name, used in places like frontend UI. This is not a unique identifier.`;
4
+ const _clientMetaDataDescription = (identify) => `Client metadata. Used as a data store, accessible from the client side. Do not store information that should not be exposed to the client.`;
5
+ const _serverMetaDataDescription = (identify) => `Server metadata. Used as a data store, only accessible from the server side. You can store secret information related to the ${identify} here.`;
6
+ const _atMillisDescription = (identify) => `(the number of milliseconds since epoch, January 1, 1970, UTC)`;
7
+ const _createdAtMillisDescription = (identify) => `The time the ${identify} was created ${_atMillisDescription(identify)}`;
8
+ const _updatedAtMillisDescription = (identify) => `The time the ${identify} was last updated ${_atMillisDescription(identify)}`;
9
+ // Built-in replacements
10
+ /* eslint-disable no-restricted-syntax */
11
+ export function yupString(...args) {
12
+ return yup.string(...args);
13
+ }
14
+ export function yupNumber(...args) {
15
+ return yup.number(...args);
16
+ }
17
+ export function yupBoolean(...args) {
18
+ return yup.boolean(...args);
19
+ }
20
+ /**
21
+ * @deprecated, use number of milliseconds since epoch instead
22
+ */
23
+ export function yupDate(...args) {
24
+ return yup.date(...args);
25
+ }
26
+ export function yupMixed(...args) {
27
+ return yup.mixed(...args);
28
+ }
29
+ export function yupArray(...args) {
30
+ return yup.array(...args);
31
+ }
32
+ export function yupTuple(...args) {
33
+ return yup.tuple(...args);
34
+ }
35
+ export function yupObject(...args) {
36
+ const object = yup.object(...args).test('no-unknown-object-properties', ({ path }) => `${path} contains unknown properties`, (value, context) => {
37
+ if (context.options.context?.noUnknownPathPrefixes?.some((prefix) => context.path.startsWith(prefix))) {
38
+ const availableKeys = new Set(Object.keys(context.schema.fields));
39
+ const unknownKeys = Object.keys(value ?? {}).filter(key => !availableKeys.has(key));
40
+ if (unknownKeys.length > 0) {
41
+ // TODO "did you mean XYZ"
42
+ return context.createError({
43
+ message: `${context.path} contains unknown properties: ${unknownKeys.join(', ')}`,
44
+ path: context.path,
45
+ params: { unknownKeys },
46
+ });
47
+ }
48
+ }
49
+ return true;
50
+ });
51
+ // we don't want to update the type of `object` to have a default flag
52
+ return object.default(undefined);
53
+ }
54
+ /* eslint-enable no-restricted-syntax */
3
55
  // Common
4
- export const adaptSchema = yup.mixed();
56
+ export const adaptSchema = yupMixed();
5
57
  /**
6
58
  * Yup's URL schema does not recognize some URLs (including `http://localhost`) as a valid URL. This schema is a workaround for that.
7
59
  */
8
- export const urlSchema = yup.string().test({
60
+ export const urlSchema = yupString().test({
9
61
  name: 'url',
10
62
  message: 'Invalid URL',
11
63
  test: (value) => {
@@ -20,14 +72,54 @@ export const urlSchema = yup.string().test({
20
72
  }
21
73
  },
22
74
  });
75
+ export const jsonSchema = yupMixed().nullable().defined().transform((value) => JSON.parse(JSON.stringify(value)));
76
+ export const jsonStringSchema = yupString().test("json", "Invalid JSON format", (value) => {
77
+ if (value == null)
78
+ return true;
79
+ try {
80
+ JSON.parse(value);
81
+ return true;
82
+ }
83
+ catch (error) {
84
+ return false;
85
+ }
86
+ });
87
+ export const emailSchema = yupString().email();
23
88
  // Request auth
24
- export const clientOrHigherAuthTypeSchema = yup.string().oneOf(['client', 'server', 'admin']);
25
- export const serverOrHigherAuthTypeSchema = yup.string().oneOf(['server', 'admin']);
26
- export const adminAuthTypeSchema = yup.string().oneOf(['admin']);
89
+ export const clientOrHigherAuthTypeSchema = yupString().oneOf(['client', 'server', 'admin']);
90
+ export const serverOrHigherAuthTypeSchema = yupString().oneOf(['server', 'admin']);
91
+ export const adminAuthTypeSchema = yupString().oneOf(['admin']);
27
92
  // Projects
28
- export const projectIdSchema = yup.string().meta({ openapiField: { description: 'Project ID as retrieved on Stack\'s dashboard', exampleValue: 'project-id' } });
93
+ export const projectIdSchema = yupString().meta({ openapiField: { description: _idDescription('project'), exampleValue: 'e0b52f4d-dece-408c-af49-d23061bb0f8d' } });
94
+ export const projectDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('project'), exampleValue: 'MyMusic' } });
95
+ export const projectDescriptionSchema = yupString().nullable().meta({ openapiField: { description: 'A human readable description of the project', exampleValue: 'A music streaming service' } });
96
+ export const projectCreatedAtMillisSchema = yupNumber().meta({ openapiField: { description: _createdAtMillisDescription('project'), exampleValue: 1630000000000 } });
97
+ export const projectUserCountSchema = yupNumber().meta({ openapiField: { description: 'The number of users in this project', exampleValue: 10 } });
98
+ export const projectIsProductionModeSchema = yupBoolean().meta({ openapiField: { description: 'Whether the project is in production mode', exampleValue: true } });
99
+ // Project config
100
+ export const projectConfigIdSchema = yupString().meta({ openapiField: { description: _idDescription('project config'), exampleValue: 'd09201f0-54f5-40bd-89ff-6d1815ddad24' } });
101
+ 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 } });
102
+ export const projectCreateTeamOnSignUpSchema = yupBoolean().meta({ openapiField: { description: 'Whether a team should be created for each user that signs up', exampleValue: true } });
103
+ export const projectMagicLinkEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether magic link authentication is enabled for this project', exampleValue: true } });
104
+ export const projectCredentialEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether email password authentication is enabled for this project', exampleValue: true } });
105
+ // Project OAuth config
106
+ export const oauthIdSchema = yupString().oneOf(['google', 'github', 'facebook', 'microsoft', 'spotify']).meta({ openapiField: { description: 'OAuth provider ID, one of google, github, facebook, microsoft, spotify', exampleValue: 'google' } });
107
+ export const oauthEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether the OAuth provider is enabled. If an provider is first enabled, then disabled, it will be shown in the list but with enabled=false', exampleValue: true } });
108
+ export const oauthTypeSchema = yupString().oneOf(['shared', 'standard']).meta({ openapiField: { description: 'OAuth provider type, one of shared, standard. "shared" uses Stack shared OAuth keys and it is only meant for development. "standard" uses your own OAuth keys and will show your logo and company name when signing in with the provider.', exampleValue: 'standard' } });
109
+ export const oauthClientIdSchema = yupString().meta({ openapiField: { description: 'OAuth client ID. Needs to be specified when using type="standard"', exampleValue: 'google-oauth-client-id' } });
110
+ export const oauthClientSecretSchema = yupString().meta({ openapiField: { description: 'OAuth client secret. Needs to be specified when using type="standard"', exampleValue: 'google-oauth-client-secret' } });
111
+ // Project email config
112
+ export const emailTypeSchema = yupString().oneOf(['shared', 'standard']).meta({ openapiField: { description: 'Email provider type, one of shared, standard. "shared" uses Stack shared email provider and it is only meant for development. "standard" uses your own email server and will have your email address as the sender.', exampleValue: 'standard' } });
113
+ export const emailSenderNameSchema = yupString().meta({ openapiField: { description: 'Email sender name. Needs to be specified when using type="standard"', exampleValue: 'Stack' } });
114
+ export const emailHostSchema = yupString().meta({ openapiField: { description: 'Email host. Needs to be specified when using type="standard"', exampleValue: 'smtp.your-domain.com' } });
115
+ export const emailPortSchema = yupNumber().meta({ openapiField: { description: 'Email port. Needs to be specified when using type="standard"', exampleValue: 587 } });
116
+ export const emailUsernameSchema = yupString().meta({ openapiField: { description: 'Email username. Needs to be specified when using type="standard"', exampleValue: 'smtp-email' } });
117
+ export const emailSenderEmailSchema = emailSchema.meta({ openapiField: { description: 'Email sender email. Needs to be specified when using type="standard"', exampleValue: 'example@your-domain.com' } });
118
+ export const emailPasswordSchema = yupString().meta({ openapiField: { description: 'Email password. Needs to be specified when using type="standard"', exampleValue: 'your-email-password' } });
119
+ // Project domain config
120
+ export const domainSchema = yupString().test('is-https', 'Domain must start with https://', (value) => value?.startsWith('https://')).meta({ openapiField: { description: 'Your domain URL. Make sure you own and trust this domain. Needs to start with https://', exampleValue: 'example.com' } });
121
+ export const handlerPathSchema = yupString().test('is-handler-path', 'Handler path must start with /', (value) => value?.startsWith('/')).meta({ openapiField: { description: 'Handler path. If you did not setup a custom handler path, it should be "/handler" by default. It needs to start with /', exampleValue: '/handler' } });
29
122
  // Users
30
- export const userIdRequestSchema = yup.string().uuid().meta({ openapiField: { description: 'The ID of the user', exampleValue: '3241a285-8329-4d69-8f3d-316e08cf140c' } });
31
123
  export class ReplaceFieldWithOwnUserId extends Error {
32
124
  constructor(path) {
33
125
  super(`This error should be caught by whoever validated the schema, and the field in the path '${path}' should be replaced with the current user's id. This is a workaround to yup not providing access to the context inside the transform function.`);
@@ -35,7 +127,7 @@ export class ReplaceFieldWithOwnUserId extends Error {
35
127
  }
36
128
  }
37
129
  const userIdMeSentinelUuid = "cad564fd-f81b-43f4-b390-98abf3fcc17e";
38
- export const userIdOrMeRequestSchema = yup.string().uuid().transform(v => {
130
+ export const userIdOrMeSchema = yupString().uuid().transform(v => {
39
131
  if (v === "me")
40
132
  return userIdMeSentinelUuid;
41
133
  else
@@ -45,23 +137,62 @@ export const userIdOrMeRequestSchema = yup.string().uuid().transform(v => {
45
137
  throw new ReplaceFieldWithOwnUserId(context.path);
46
138
  return true;
47
139
  }).meta({ openapiField: { description: 'The ID of the user, or the special value `me` to signify the currently authenticated user', exampleValue: '3241a285-8329-4d69-8f3d-316e08cf140c' } });
48
- export const userIdResponseSchema = yup.string().uuid().meta({ openapiField: { description: 'The immutable user ID used to uniquely identify this user', exampleValue: '3241a285-8329-4d69-8f3d-316e08cf140c' } });
49
- export const primaryEmailSchema = yup.string().email().meta({ openapiField: { description: 'Primary email', exampleValue: 'johndoe@example.com' } });
50
- export const primaryEmailVerifiedSchema = yup.boolean().meta({ openapiField: { description: 'Whether the primary email has been verified to belong to this user', exampleValue: true } });
51
- export const userDisplayNameSchema = yup.string().nullable().meta({ openapiField: { description: 'Human-readable display name', exampleValue: 'John Doe' } });
52
- export const selectedTeamIdSchema = yup.string().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
53
- export const profileImageUrlSchema = yup.string().meta({ openapiField: { description: 'Profile image URL', exampleValue: 'https://example.com/image.jpg' } });
54
- export const signedUpAtMillisSchema = yup.number().meta({ openapiField: { description: 'Signed up at milliseconds', exampleValue: 1630000000000 } });
55
- export const userClientMetadataSchema = yupJson.meta({ openapiField: { description: 'Client metadata. Used as a data store, accessible from the client side', exampleValue: { key: 'value' } } });
56
- export const userServerMetadataSchema = yupJson.meta({ openapiField: { description: 'Server metadata. Used as a data store, only accessible from the server side', exampleValue: { key: 'value' } } });
140
+ export const userIdSchema = yupString().uuid().meta({ openapiField: { description: _idDescription('user'), exampleValue: '3241a285-8329-4d69-8f3d-316e08cf140c' } });
141
+ export const primaryEmailSchema = emailSchema.meta({ openapiField: { description: 'Primary email', exampleValue: 'johndoe@example.com' } });
142
+ export const primaryEmailVerifiedSchema = yupBoolean().meta({ openapiField: { description: 'Whether the primary email has been verified to belong to this user', exampleValue: true } });
143
+ export const userDisplayNameSchema = yupString().nullable().meta({ openapiField: { description: _displayNameDescription('user'), exampleValue: 'John Doe' } });
144
+ export const selectedTeamIdSchema = yupString().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
145
+ export const profileImageUrlSchema = yupString().meta({ openapiField: { description: 'Profile image URL', exampleValue: 'https://example.com/image.jpg' } });
146
+ export const signedUpAtMillisSchema = yupNumber().meta({ openapiField: { description: 'Signed up at milliseconds', exampleValue: 1630000000000 } });
147
+ export const userClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('user'), exampleValue: { key: 'value' } } });
148
+ export const userServerMetadataSchema = jsonSchema.meta({ openapiField: { description: _serverMetaDataDescription('user'), exampleValue: { key: 'value' } } });
57
149
  // Auth
58
- export const signInEmailSchema = yup.string().email().meta({ openapiField: { description: 'The email to sign in with.', exampleValue: 'johndoe@example.com' } });
59
- export const verificationLinkRedirectUrlSchema = urlSchema.meta({ openapiField: { description: 'The URL to redirect to after the user has verified their email. A query argument `code` with the verification code will be appended to it.', exampleValue: 'https://example.com/handler' } });
60
- export const accessTokenResponseSchema = yup.string().meta({ openapiField: { description: 'Short-lived access token that can be used to authenticate the user', exampleValue: 'eyJhmMiJBMTO...diI4QT' } });
61
- export const refreshTokenResponseSchema = yup.string().meta({ openapiField: { description: 'Long-lived refresh token that can be used to obtain a new access token', exampleValue: 'i8nsoaq2...14y' } });
62
- export const signInResponseSchema = yup.object({
150
+ export const signInEmailSchema = emailSchema.meta({ openapiField: { description: 'The email to sign in with.', exampleValue: 'johndoe@example.com' } });
151
+ 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' } });
152
+ export const emailVerificationCallbackUrlSchema = 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' } });
153
+ export const accessTokenResponseSchema = yupString().meta({ openapiField: { description: 'Short-lived access token that can be used to authenticate the user', exampleValue: 'eyJhmMiJBMTO...diI4QT' } });
154
+ export const refreshTokenResponseSchema = yupString().meta({ openapiField: { description: 'Long-lived refresh token that can be used to obtain a new access token', exampleValue: 'i8nsoaq2...14y' } });
155
+ export const signInResponseSchema = yupObject({
63
156
  refresh_token: refreshTokenResponseSchema.required(),
64
157
  access_token: accessTokenResponseSchema.required(),
65
- is_new_user: yup.boolean().meta({ openapiField: { description: 'Whether the user is a new user', exampleValue: true } }).required(),
66
- user_id: userIdResponseSchema.required(),
158
+ is_new_user: yupBoolean().meta({ openapiField: { description: 'Whether the user is a new user', exampleValue: true } }).required(),
159
+ user_id: userIdSchema.required(),
67
160
  });
161
+ // Permissions
162
+ export const teamSystemPermissions = [
163
+ '$update_team',
164
+ '$delete_team',
165
+ '$read_members',
166
+ '$remove_members',
167
+ '$invite_members',
168
+ ];
169
+ export const teamPermissionIdSchema = yupString()
170
+ .matches(/^\$?[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" and optional "$" at the beginning are allowed')
171
+ .test('is-system-permission', 'System permissions must start with a dollar sign', (value, ctx) => {
172
+ if (!value)
173
+ return true;
174
+ if (value.startsWith('$') && !teamSystemPermissions.includes(value)) {
175
+ return ctx.createError({ message: 'Invalid system permission' });
176
+ }
177
+ return true;
178
+ })
179
+ .meta({ openapiField: { description: `The permission ID used to uniquely identify a permission. Can either be a custom permission with lowercase letters, numbers, ":", and "_" characters, or one of the system permissions: ${teamSystemPermissions.join(', ')}`, exampleValue: '$read_secret_info' } });
180
+ export const customTeamPermissionIdSchema = yupString()
181
+ .matches(/^[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" are allowed')
182
+ .meta({ openapiField: { description: 'The permission ID used to uniquely identify a permission. Can only contain lowercase letters, numbers, ":", and "_" characters', exampleValue: 'read_secret_info' } });
183
+ export const teamPermissionDescriptionSchema = yupString().meta({ openapiField: { description: 'A human-readable description of the permission', exampleValue: 'Read secret information' } });
184
+ export const containedPermissionIdsSchema = yupArray(teamPermissionIdSchema.required()).meta({ openapiField: { description: 'The IDs of the permissions that are contained in this permission', exampleValue: ['read_public_info'] } });
185
+ // Teams
186
+ export const teamIdSchema = yupString().uuid().meta({ openapiField: { description: _idDescription('team'), exampleValue: 'ad962777-8244-496a-b6a2-e0c6a449c79e' } });
187
+ export const teamDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('team'), exampleValue: 'My Team' } });
188
+ export const teamClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('team'), exampleValue: { key: 'value' } } });
189
+ export const teamServerMetadataSchema = jsonSchema.meta({ openapiField: { description: _serverMetaDataDescription('team'), exampleValue: { key: 'value' } } });
190
+ export const teamCreatedAtMillisSchema = yupNumber().meta({ openapiField: { description: _createdAtMillisDescription('team'), exampleValue: 1630000000000 } });
191
+ // Utils
192
+ export function yupRequiredWhen(schema, triggerName, isValue) {
193
+ return schema.when(triggerName, {
194
+ is: isValue,
195
+ then: (schema) => schema.required(),
196
+ otherwise: (schema) => schema.optional()
197
+ });
198
+ }
@@ -42,6 +42,7 @@ export declare class InternalSession {
42
42
  refreshToken: string | null;
43
43
  accessToken?: string | null;
44
44
  }): string;
45
+ isKnownToBeInvalid(): boolean;
45
46
  /**
46
47
  * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.
47
48
  */
package/dist/sessions.js CHANGED
@@ -1,12 +1,19 @@
1
+ import { StackAssertionError } from "./utils/errors";
1
2
  import { Store } from "./utils/stores";
2
3
  export class AccessToken {
3
4
  constructor(token) {
4
5
  this.token = token;
6
+ if (token === "undefined") {
7
+ throw new StackAssertionError("Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!");
8
+ }
5
9
  }
6
10
  }
7
11
  export class RefreshToken {
8
12
  constructor(token) {
9
13
  this.token = token;
14
+ if (token === "undefined") {
15
+ throw new StackAssertionError("Refresh token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!");
16
+ }
10
17
  }
11
18
  }
12
19
  /**
@@ -39,6 +46,9 @@ export class InternalSession {
39
46
  return "not-logged-in";
40
47
  }
41
48
  }
49
+ isKnownToBeInvalid() {
50
+ return this._knownToBeInvalid.get();
51
+ }
42
52
  /**
43
53
  * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.
44
54
  */
@@ -88,13 +98,13 @@ export class InternalSession {
88
98
  * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
89
99
  */
90
100
  async _getPotentiallyExpiredAccessToken() {
91
- const oldAccessToken = this._accessToken.get();
92
- if (oldAccessToken)
93
- return oldAccessToken;
94
101
  if (!this._refreshToken)
95
102
  return null;
96
103
  if (this._knownToBeInvalid.get())
97
104
  return null;
105
+ const oldAccessToken = this._accessToken.get();
106
+ if (oldAccessToken)
107
+ return oldAccessToken;
98
108
  // refresh access token
99
109
  if (!this._refreshPromise) {
100
110
  this._refreshAndSetRefreshPromise(this._refreshToken);
@@ -1,4 +1,6 @@
1
1
  /**
2
- * Returns the first argument passed to it, but compilers won't be able to optimize it out. This is useful in some cases where compiler warnings go awry; for example, when importing things that may not exist (but are guaranteed to exist at runtime).
2
+ * Returns the first argument passed to it, but compilers won't be able to optimize it out. This is useful in some
3
+ * cases where compiler warnings go awry; for example, when importing things that may not exist (but are guaranteed
4
+ * to exist at runtime).
3
5
  */
4
6
  export declare function scrambleDuringCompileTime<T>(t: T): T;
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Returns the first argument passed to it, but compilers won't be able to optimize it out. This is useful in some cases where compiler warnings go awry; for example, when importing things that may not exist (but are guaranteed to exist at runtime).
2
+ * Returns the first argument passed to it, but compilers won't be able to optimize it out. This is useful in some
3
+ * cases where compiler warnings go awry; for example, when importing things that may not exist (but are guaranteed
4
+ * to exist at runtime).
3
5
  */
4
6
  export function scrambleDuringCompileTime(t) {
5
7
  return t;
@@ -13,8 +13,11 @@ type Status = {
13
13
  message: string;
14
14
  };
15
15
  type StatusErrorConstructorParameters = [
16
- statusCode: number | Status,
16
+ status: Status,
17
17
  message?: string
18
+ ] | [
19
+ statusCode: number | Status,
20
+ message: string
18
21
  ];
19
22
  export declare class StatusError extends Error {
20
23
  name: string;
@@ -185,6 +188,10 @@ export declare class StatusError extends Error {
185
188
  getStatusCode(): number;
186
189
  getBody(): Uint8Array;
187
190
  getHeaders(): Record<string, string[]>;
191
+ toDescriptiveJson(): Json;
192
+ /**
193
+ * @deprecated this is not a good way to make status errors human-readable, use toDescriptiveJson instead
194
+ */
188
195
  toHttpJson(): Json;
189
196
  }
190
197
  export {};
@@ -27,11 +27,11 @@ export function registerErrorSink(sink) {
27
27
  errorSinks.add(sink);
28
28
  }
29
29
  registerErrorSink((location, ...args) => {
30
- console.error(`Error in ${location}:`, ...args);
30
+ console.error(`\x1b[41mError in ${location}:`, ...args, "\x1b[0m");
31
31
  });
32
- registerErrorSink((location, error, ...args) => {
32
+ registerErrorSink((location, error, ...extraArgs) => {
33
33
  globalVar.stackCapturedErrors = globalVar.stackCapturedErrors ?? [];
34
- globalVar.stackCapturedErrors.push({ location, error: args, extraArgs: args });
34
+ globalVar.stackCapturedErrors.push({ location, error, extraArgs });
35
35
  });
36
36
  export function captureError(location, error) {
37
37
  for (const sink of errorSinks) {
@@ -44,10 +44,12 @@ export class StatusError extends Error {
44
44
  message ??= status.message;
45
45
  status = status.statusCode;
46
46
  }
47
- message ??= "Server Error";
48
47
  super(message);
49
48
  this.name = "StatusError";
50
49
  this.statusCode = status;
50
+ if (!message) {
51
+ throw new StackAssertionError("StatusError always requires a message unless a Status object is passed", {}, { cause: this });
52
+ }
51
53
  }
52
54
  isClientError() {
53
55
  return this.statusCode >= 400 && this.statusCode < 500;
@@ -66,6 +68,16 @@ export class StatusError extends Error {
66
68
  "Content-Type": ["text/plain; charset=utf-8"],
67
69
  };
68
70
  }
71
+ toDescriptiveJson() {
72
+ return {
73
+ status_code: this.getStatusCode(),
74
+ message: this.message,
75
+ headers: this.getHeaders(),
76
+ };
77
+ }
78
+ /**
79
+ * @deprecated this is not a good way to make status errors human-readable, use toDescriptiveJson instead
80
+ */
69
81
  toHttpJson() {
70
82
  return {
71
83
  status_code: this.statusCode,
@@ -114,3 +126,4 @@ StatusError.InsufficientStorage = { statusCode: 507, message: "Insufficient Stor
114
126
  StatusError.LoopDetected = { statusCode: 508, message: "Loop Detected" };
115
127
  StatusError.NotExtended = { statusCode: 510, message: "Not Extended" };
116
128
  StatusError.NetworkAuthenticationRequired = { statusCode: 511, message: "Network Authentication Required" };
129
+ StatusError.prototype.name = "StatusError";
@@ -6,7 +6,9 @@ export type DeepPartial<T> = T extends object ? {
6
6
  *
7
7
  * Note that since they are assumed to be plain objects, this function does not compare prototypes.
8
8
  */
9
- export declare function deepPlainEquals<T>(obj1: T, obj2: unknown): obj2 is T;
9
+ export declare function deepPlainEquals<T>(obj1: T, obj2: unknown, options?: {
10
+ ignoreUndefinedValues?: boolean;
11
+ }): obj2 is T;
10
12
  export declare function deepPlainClone<T>(obj: T): T;
11
13
  export declare function deepPlainSnakeCaseToCamelCase(snakeCaseObj: any): any;
12
14
  export declare function deepPlainCamelCaseToSnakeCase(camelCaseObj: any): any;
@@ -27,3 +29,4 @@ export type FilterUndefined<T> = {
27
29
  export declare function filterUndefined<T extends {}>(obj: T): FilterUndefined<T>;
28
30
  export declare function pick<T extends {}, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>;
29
31
  export declare function omit<T extends {}, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>;
32
+ export declare function split<T extends {}, K extends keyof T>(obj: T, keys: K[]): [Pick<T, K>, Omit<T, K>];
@@ -5,7 +5,7 @@ import { camelCaseToSnakeCase, snakeCaseToCamelCase } from "./strings";
5
5
  *
6
6
  * Note that since they are assumed to be plain objects, this function does not compare prototypes.
7
7
  */
8
- export function deepPlainEquals(obj1, obj2) {
8
+ export function deepPlainEquals(obj1, obj2, options = {}) {
9
9
  if (typeof obj1 !== typeof obj2)
10
10
  return false;
11
11
  if (obj1 === obj2)
@@ -19,13 +19,18 @@ export function deepPlainEquals(obj1, obj2) {
19
19
  return false;
20
20
  if (obj1.length !== obj2.length)
21
21
  return false;
22
- return obj1.every((v, i) => deepPlainEquals(v, obj2[i]));
22
+ return obj1.every((v, i) => deepPlainEquals(v, obj2[i], options));
23
23
  }
24
- const keys1 = Object.keys(obj1);
25
- const keys2 = Object.keys(obj2);
26
- if (keys1.length !== keys2.length)
24
+ const entries1 = Object.entries(obj1).filter(([k, v]) => !options.ignoreUndefinedValues || v !== undefined);
25
+ const entries2 = Object.entries(obj2).filter(([k, v]) => !options.ignoreUndefinedValues || v !== undefined);
26
+ if (entries1.length !== entries2.length)
27
27
  return false;
28
- return keys1.every((k) => k in obj2 && deepPlainEquals(obj1[k], obj2[k]));
28
+ return entries1.every(([k, v1]) => {
29
+ const e2 = entries2.find(([k2]) => k === k2);
30
+ if (!e2)
31
+ return false;
32
+ return deepPlainEquals(v1, e2[1], options);
33
+ });
29
34
  }
30
35
  case 'undefined':
31
36
  case 'string':
@@ -58,7 +63,7 @@ export function deepPlainSnakeCaseToCamelCase(snakeCaseObj) {
58
63
  if (typeof snakeCaseObj !== 'object' || !snakeCaseObj)
59
64
  return snakeCaseObj;
60
65
  if (Array.isArray(snakeCaseObj))
61
- return snakeCaseObj.map(deepPlainSnakeCaseToCamelCase);
66
+ return snakeCaseObj.map(o => deepPlainSnakeCaseToCamelCase(o));
62
67
  return Object.fromEntries(Object.entries(snakeCaseObj).map(([k, v]) => [snakeCaseToCamelCase(k), deepPlainSnakeCaseToCamelCase(v)]));
63
68
  }
64
69
  export function deepPlainCamelCaseToSnakeCase(camelCaseObj) {
@@ -67,7 +72,7 @@ export function deepPlainCamelCaseToSnakeCase(camelCaseObj) {
67
72
  if (typeof camelCaseObj !== 'object' || !camelCaseObj)
68
73
  return camelCaseObj;
69
74
  if (Array.isArray(camelCaseObj))
70
- return camelCaseObj.map(deepPlainCamelCaseToSnakeCase);
75
+ return camelCaseObj.map(o => deepPlainCamelCaseToSnakeCase(o));
71
76
  return Object.fromEntries(Object.entries(camelCaseObj).map(([k, v]) => [camelCaseToSnakeCase(k), deepPlainCamelCaseToSnakeCase(v)]));
72
77
  }
73
78
  export function typedEntries(obj) {
@@ -98,3 +103,6 @@ export function pick(obj, keys) {
98
103
  export function omit(obj, keys) {
99
104
  return Object.fromEntries(Object.entries(obj).filter(([k]) => !keys.includes(k)));
100
105
  }
106
+ export function split(obj, keys) {
107
+ return [pick(obj, keys), omit(obj, keys)];
108
+ }
@@ -159,7 +159,12 @@ export function rateLimited(func, options) {
159
159
  const end = performance.now();
160
160
  waitUntil = Math.max(waitUntil, start + (options.throttleMs ?? 0), end + (options.gapMs ?? 0));
161
161
  for (const nextFunc of nextFuncs) {
162
- value.status === "ok" ? nextFunc[0](value.data) : nextFunc[1](value.error);
162
+ if (value.status === "ok") {
163
+ nextFunc[0](value.data);
164
+ }
165
+ else {
166
+ nextFunc[1](value.error);
167
+ }
163
168
  }
164
169
  };
165
170
  runAsynchronously(async () => {
@@ -1 +1,2 @@
1
1
  export declare function logged<T extends object>(name: string, toLog: T, options?: {}): T;
2
+ export declare function createLazyProxy<FactoryResult>(factory: () => FactoryResult): FactoryResult;
@@ -57,3 +57,68 @@ export function logged(name, toLog, options = {}) {
57
57
  });
58
58
  return proxy;
59
59
  }
60
+ export function createLazyProxy(factory) {
61
+ let cache = undefined;
62
+ let initialized = false;
63
+ function initializeIfNeeded() {
64
+ if (!initialized) {
65
+ cache = factory();
66
+ initialized = true;
67
+ }
68
+ return cache;
69
+ }
70
+ return new Proxy({}, {
71
+ get(target, prop, receiver) {
72
+ const instance = initializeIfNeeded();
73
+ return Reflect.get(instance, prop, receiver);
74
+ },
75
+ set(target, prop, value, receiver) {
76
+ const instance = initializeIfNeeded();
77
+ return Reflect.set(instance, prop, value, receiver);
78
+ },
79
+ has(target, prop) {
80
+ const instance = initializeIfNeeded();
81
+ return Reflect.has(instance, prop);
82
+ },
83
+ deleteProperty(target, prop) {
84
+ const instance = initializeIfNeeded();
85
+ return Reflect.deleteProperty(instance, prop);
86
+ },
87
+ ownKeys(target) {
88
+ const instance = initializeIfNeeded();
89
+ return Reflect.ownKeys(instance);
90
+ },
91
+ getOwnPropertyDescriptor(target, prop) {
92
+ const instance = initializeIfNeeded();
93
+ return Reflect.getOwnPropertyDescriptor(instance, prop);
94
+ },
95
+ defineProperty(target, prop, descriptor) {
96
+ const instance = initializeIfNeeded();
97
+ return Reflect.defineProperty(instance, prop, descriptor);
98
+ },
99
+ getPrototypeOf(target) {
100
+ const instance = initializeIfNeeded();
101
+ return Reflect.getPrototypeOf(instance);
102
+ },
103
+ setPrototypeOf(target, proto) {
104
+ const instance = initializeIfNeeded();
105
+ return Reflect.setPrototypeOf(instance, proto);
106
+ },
107
+ isExtensible(target) {
108
+ const instance = initializeIfNeeded();
109
+ return Reflect.isExtensible(instance);
110
+ },
111
+ preventExtensions(target) {
112
+ const instance = initializeIfNeeded();
113
+ return Reflect.preventExtensions(instance);
114
+ },
115
+ apply(target, thisArg, argumentsList) {
116
+ const instance = initializeIfNeeded();
117
+ return Reflect.apply(instance, thisArg, argumentsList);
118
+ },
119
+ construct(target, argumentsList, newTarget) {
120
+ const instance = initializeIfNeeded();
121
+ return Reflect.construct(instance, argumentsList, newTarget);
122
+ }
123
+ });
124
+ }
@@ -1,4 +1,4 @@
1
- /// <reference types="react" />
1
+ import React from "react";
2
2
  export declare function getNodeText(node: React.ReactNode): string;
3
3
  /**
4
4
  * Suspends the currently rendered component indefinitely. Will not unsuspend unless the component rerenders.
@@ -1,4 +1,4 @@
1
- import { use } from "react";
1
+ import React from "react";
2
2
  import { neverResolve } from "./promises";
3
3
  import { deindent } from "./strings";
4
4
  import { isBrowserLike } from "./env";
@@ -23,7 +23,7 @@ export function getNodeText(node) {
23
23
  * You can use this to translate older query- or AsyncResult-based code to new the Suspense system, for example: `if (query.isLoading) suspend();`
24
24
  */
25
25
  export function suspend() {
26
- use(neverResolve());
26
+ React.use(neverResolve());
27
27
  throw new Error("Somehow a Promise that never resolves was resolved?");
28
28
  }
29
29
  /**
@@ -107,12 +107,12 @@ export function mergeScopeStrings(...scopes) {
107
107
  }
108
108
  export function snakeCaseToCamelCase(snakeCase) {
109
109
  if (snakeCase.match(/[A-Z]/))
110
- return snakeCase; // TODO: this is a hack for fixing the email templates, remove this after v2 migration
110
+ return snakeCase; // TODO next-release: this is a hack for fixing the email templates, remove this after v2 migration
111
111
  return snakeCase.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
112
112
  }
113
113
  export function camelCaseToSnakeCase(camelCase) {
114
114
  if (camelCase.match(/_/))
115
- return camelCase; // TODO: this is a hack for fixing the email templates, remove this after v2 migration
115
+ return camelCase; // TODO next-release: this is a hack for fixing the email templates, remove this after v2 migration
116
116
  return camelCase.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
117
117
  }
118
118
  /**
@@ -256,7 +256,7 @@ function nicifyPropertyString(str) {
256
256
  return JSON.stringify(str);
257
257
  }
258
258
  function getNicifiableKeys(value) {
259
- return ("getNicifiableKeys" in value ? value.getNicifiableKeys : null)?.() ?? Object.keys(value).sort();
259
+ return ("getNicifiableKeys" in value ? value.getNicifiableKeys?.bind(value) : null)?.() ?? Object.keys(value).sort();
260
260
  }
261
261
  function getNicifiableEntries(value) {
262
262
  const recordLikes = [Headers];
@@ -0,0 +1 @@
1
+ export declare function isLocalhost(urlOrString: string | URL): boolean;
@@ -0,0 +1,8 @@
1
+ export function isLocalhost(urlOrString) {
2
+ const url = new URL(urlOrString);
3
+ if (url.hostname === "localhost" || url.hostname.endsWith(".localhost"))
4
+ return true;
5
+ if (url.hostname.match(/^127\.\d+\.\d+\.\d+$/))
6
+ return true;
7
+ return false;
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.5.3",
3
+ "version": "2.5.5",
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.3"
39
+ "@stackframe/stack-sc": "2.5.5"
40
40
  },
41
41
  "devDependencies": {
42
42
  "rimraf": "^5.0.5",
@@ -1,3 +0,0 @@
1
- import * as yup from "yup";
2
- export declare const yupJson: yup.MixedSchema<{} | null, yup.AnyObject, undefined, "">;
3
- export declare const yupJsonValidator: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
package/dist/utils/yup.js DELETED
@@ -1,13 +0,0 @@
1
- import * as yup from "yup";
2
- export const yupJson = yup.mixed().nullable().defined().transform((value) => JSON.parse(JSON.stringify(value)));
3
- export const yupJsonValidator = yup.string().test("json", "Invalid JSON format", (value) => {
4
- if (!value)
5
- return true;
6
- try {
7
- JSON.parse(value);
8
- return true;
9
- }
10
- catch (error) {
11
- return false;
12
- }
13
- });