@stackframe/stack-shared 2.8.2 → 2.8.3

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.
@@ -48,6 +48,8 @@ export declare const projectsCrudAdminReadSchema: import("yup").ObjectSchema<{
48
48
  passkey_enabled: boolean;
49
49
  client_team_creation_enabled: boolean;
50
50
  client_user_deletion_enabled: boolean;
51
+ allow_user_api_keys: boolean;
52
+ allow_team_api_keys: boolean;
51
53
  oauth_providers: {
52
54
  client_id?: string | undefined;
53
55
  client_secret?: string | undefined;
@@ -101,6 +103,8 @@ export declare const projectsCrudAdminReadSchema: import("yup").ObjectSchema<{
101
103
  passkey_enabled: undefined;
102
104
  client_team_creation_enabled: undefined;
103
105
  client_user_deletion_enabled: undefined;
106
+ allow_user_api_keys: undefined;
107
+ allow_team_api_keys: undefined;
104
108
  oauth_providers: undefined;
105
109
  enabled_oauth_providers: undefined;
106
110
  domains: undefined;
@@ -130,6 +134,8 @@ export declare const projectsCrudClientReadSchema: import("yup").ObjectSchema<{
130
134
  passkey_enabled: boolean;
131
135
  client_team_creation_enabled: boolean;
132
136
  client_user_deletion_enabled: boolean;
137
+ allow_user_api_keys: boolean;
138
+ allow_team_api_keys: boolean;
133
139
  enabled_oauth_providers: {
134
140
  id: "google" | "github" | "microsoft" | "spotify" | "facebook" | "discord" | "gitlab" | "bitbucket" | "linkedin" | "apple" | "x";
135
141
  }[];
@@ -144,6 +150,8 @@ export declare const projectsCrudClientReadSchema: import("yup").ObjectSchema<{
144
150
  passkey_enabled: undefined;
145
151
  client_team_creation_enabled: undefined;
146
152
  client_user_deletion_enabled: undefined;
153
+ allow_user_api_keys: undefined;
154
+ allow_team_api_keys: undefined;
147
155
  enabled_oauth_providers: undefined;
148
156
  };
149
157
  }, "">;
@@ -159,6 +167,8 @@ export declare const projectsCrudAdminUpdateSchema: import("yup").ObjectSchema<{
159
167
  passkey_enabled?: boolean | undefined;
160
168
  client_team_creation_enabled?: boolean | undefined;
161
169
  client_user_deletion_enabled?: boolean | undefined;
170
+ allow_user_api_keys?: boolean | undefined;
171
+ allow_team_api_keys?: boolean | undefined;
162
172
  oauth_providers?: {
163
173
  client_id?: string | undefined;
164
174
  client_secret?: string | undefined;
@@ -211,6 +221,8 @@ export declare const projectsCrudAdminCreateSchema: import("yup").ObjectSchema<{
211
221
  passkey_enabled?: boolean | undefined;
212
222
  client_team_creation_enabled?: boolean | undefined;
213
223
  client_user_deletion_enabled?: boolean | undefined;
224
+ allow_user_api_keys?: boolean | undefined;
225
+ allow_team_api_keys?: boolean | undefined;
214
226
  oauth_providers?: {
215
227
  client_id?: string | undefined;
216
228
  client_secret?: string | undefined;
@@ -265,6 +277,8 @@ export declare const projectsCrud: import("../../crud").CrudSchemaFromOptions<{
265
277
  passkey_enabled: boolean;
266
278
  client_team_creation_enabled: boolean;
267
279
  client_user_deletion_enabled: boolean;
280
+ allow_user_api_keys: boolean;
281
+ allow_team_api_keys: boolean;
268
282
  enabled_oauth_providers: {
269
283
  id: "google" | "github" | "microsoft" | "spotify" | "facebook" | "discord" | "gitlab" | "bitbucket" | "linkedin" | "apple" | "x";
270
284
  }[];
@@ -279,6 +293,8 @@ export declare const projectsCrud: import("../../crud").CrudSchemaFromOptions<{
279
293
  passkey_enabled: undefined;
280
294
  client_team_creation_enabled: undefined;
281
295
  client_user_deletion_enabled: undefined;
296
+ allow_user_api_keys: undefined;
297
+ allow_team_api_keys: undefined;
282
298
  enabled_oauth_providers: undefined;
283
299
  };
284
300
  }, "">;
@@ -298,6 +314,8 @@ export declare const projectsCrud: import("../../crud").CrudSchemaFromOptions<{
298
314
  passkey_enabled: boolean;
299
315
  client_team_creation_enabled: boolean;
300
316
  client_user_deletion_enabled: boolean;
317
+ allow_user_api_keys: boolean;
318
+ allow_team_api_keys: boolean;
301
319
  oauth_providers: {
302
320
  client_id?: string | undefined;
303
321
  client_secret?: string | undefined;
@@ -351,6 +369,8 @@ export declare const projectsCrud: import("../../crud").CrudSchemaFromOptions<{
351
369
  passkey_enabled: undefined;
352
370
  client_team_creation_enabled: undefined;
353
371
  client_user_deletion_enabled: undefined;
372
+ allow_user_api_keys: undefined;
373
+ allow_team_api_keys: undefined;
354
374
  oauth_providers: undefined;
355
375
  enabled_oauth_providers: undefined;
356
376
  domains: undefined;
@@ -382,6 +402,8 @@ export declare const projectsCrud: import("../../crud").CrudSchemaFromOptions<{
382
402
  passkey_enabled?: boolean | undefined;
383
403
  client_team_creation_enabled?: boolean | undefined;
384
404
  client_user_deletion_enabled?: boolean | undefined;
405
+ allow_user_api_keys?: boolean | undefined;
406
+ allow_team_api_keys?: boolean | undefined;
385
407
  oauth_providers?: {
386
408
  client_id?: string | undefined;
387
409
  client_secret?: string | undefined;
@@ -464,6 +486,8 @@ export declare const internalProjectsCrud: import("../../crud").CrudSchemaFromOp
464
486
  passkey_enabled: boolean;
465
487
  client_team_creation_enabled: boolean;
466
488
  client_user_deletion_enabled: boolean;
489
+ allow_user_api_keys: boolean;
490
+ allow_team_api_keys: boolean;
467
491
  oauth_providers: {
468
492
  client_id?: string | undefined;
469
493
  client_secret?: string | undefined;
@@ -517,6 +541,8 @@ export declare const internalProjectsCrud: import("../../crud").CrudSchemaFromOp
517
541
  passkey_enabled: undefined;
518
542
  client_team_creation_enabled: undefined;
519
543
  client_user_deletion_enabled: undefined;
544
+ allow_user_api_keys: undefined;
545
+ allow_team_api_keys: undefined;
520
546
  oauth_providers: undefined;
521
547
  enabled_oauth_providers: undefined;
522
548
  domains: undefined;
@@ -548,6 +574,8 @@ export declare const internalProjectsCrud: import("../../crud").CrudSchemaFromOp
548
574
  passkey_enabled?: boolean | undefined;
549
575
  client_team_creation_enabled?: boolean | undefined;
550
576
  client_user_deletion_enabled?: boolean | undefined;
577
+ allow_user_api_keys?: boolean | undefined;
578
+ allow_team_api_keys?: boolean | undefined;
551
579
  oauth_providers?: {
552
580
  client_id?: string | undefined;
553
581
  client_secret?: string | undefined;
@@ -68,6 +68,8 @@ export const projectsCrudAdminReadSchema = yupObject({
68
68
  // TODO: remove this
69
69
  client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.defined(),
70
70
  client_user_deletion_enabled: schemaFields.projectClientUserDeletionEnabledSchema.defined(),
71
+ allow_user_api_keys: schemaFields.yupBoolean().defined(),
72
+ allow_team_api_keys: schemaFields.yupBoolean().defined(),
71
73
  oauth_providers: yupArray(oauthProviderSchema.defined()).defined(),
72
74
  enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.defined()).defined().meta({ openapiField: { hidden: true } }),
73
75
  domains: yupArray(domainSchema.defined()).defined(),
@@ -89,6 +91,8 @@ export const projectsCrudClientReadSchema = yupObject({
89
91
  passkey_enabled: schemaFields.projectPasskeyEnabledSchema.defined(),
90
92
  client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.defined(),
91
93
  client_user_deletion_enabled: schemaFields.projectClientUserDeletionEnabledSchema.defined(),
94
+ allow_user_api_keys: schemaFields.yupBoolean().defined(),
95
+ allow_team_api_keys: schemaFields.yupBoolean().defined(),
92
96
  enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.defined()).defined().meta({ openapiField: { hidden: true } }),
93
97
  }).defined(),
94
98
  }).defined();
@@ -104,6 +108,8 @@ export const projectsCrudAdminUpdateSchema = yupObject({
104
108
  client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.optional(),
105
109
  client_user_deletion_enabled: schemaFields.projectClientUserDeletionEnabledSchema.optional(),
106
110
  allow_localhost: schemaFields.projectAllowLocalhostSchema.optional(),
111
+ allow_user_api_keys: schemaFields.yupBoolean().optional(),
112
+ allow_team_api_keys: schemaFields.yupBoolean().optional(),
107
113
  email_config: emailConfigSchema.optional().default(undefined),
108
114
  domains: yupArray(domainSchema.defined()).optional().default(undefined),
109
115
  oauth_providers: yupArray(oauthProviderSchema.defined()).optional().default(undefined),
@@ -40,8 +40,8 @@ export declare const teamMemberProfilesCrudServerReadSchema: import("yup").Objec
40
40
  client_read_only_metadata?: {} | null | undefined;
41
41
  server_metadata?: {} | null | undefined;
42
42
  id: string;
43
- created_at_millis: number;
44
43
  display_name: string;
44
+ created_at_millis: number;
45
45
  profile_image_url: string | null;
46
46
  } | null;
47
47
  signed_up_at_millis: number;
@@ -135,8 +135,8 @@ export declare const teamMemberProfilesCrud: import("../../crud").CrudSchemaFrom
135
135
  client_read_only_metadata?: {} | null | undefined;
136
136
  server_metadata?: {} | null | undefined;
137
137
  id: string;
138
- created_at_millis: number;
139
138
  display_name: string;
139
+ created_at_millis: number;
140
140
  profile_image_url: string | null;
141
141
  } | null;
142
142
  signed_up_at_millis: number;
@@ -43,8 +43,8 @@ export declare const usersCrudServerReadSchema: import("yup").ObjectSchema<{
43
43
  client_read_only_metadata?: {} | null | undefined;
44
44
  server_metadata?: {} | null | undefined;
45
45
  id: string;
46
- created_at_millis: number;
47
46
  display_name: string;
47
+ created_at_millis: number;
48
48
  profile_image_url: string | null;
49
49
  } | null;
50
50
  selected_team_id: string | null;
@@ -148,8 +148,8 @@ export declare const usersCrud: import("../../crud").CrudSchemaFromOptions<{
148
148
  client_read_only_metadata?: {} | null | undefined;
149
149
  server_metadata?: {} | null | undefined;
150
150
  id: string;
151
- created_at_millis: number;
152
151
  display_name: string;
152
+ created_at_millis: number;
153
153
  profile_image_url: string | null;
154
154
  } | null;
155
155
  selected_team_id: string | null;
@@ -316,8 +316,8 @@ export declare const userCreatedWebhookEvent: {
316
316
  client_read_only_metadata?: {} | null | undefined;
317
317
  server_metadata?: {} | null | undefined;
318
318
  id: string;
319
- created_at_millis: number;
320
319
  display_name: string;
320
+ created_at_millis: number;
321
321
  profile_image_url: string | null;
322
322
  } | null;
323
323
  selected_team_id: string | null;
@@ -387,8 +387,8 @@ export declare const userUpdatedWebhookEvent: {
387
387
  client_read_only_metadata?: {} | null | undefined;
388
388
  server_metadata?: {} | null | undefined;
389
389
  id: string;
390
- created_at_millis: number;
391
390
  display_name: string;
391
+ created_at_millis: number;
392
392
  profile_image_url: string | null;
393
393
  } | null;
394
394
  selected_team_id: string | null;
@@ -5,11 +5,11 @@ import { ClientInterfaceOptions, StackClientInterface } from "./clientInterface"
5
5
  import { ContactChannelsCrud } from "./crud/contact-channels";
6
6
  import { CurrentUserCrud } from "./crud/current-user";
7
7
  import { ConnectedAccountAccessTokenCrud } from "./crud/oauth";
8
+ import { ProjectPermissionsCrud } from "./crud/project-permissions";
8
9
  import { SessionsCrud } from "./crud/sessions";
9
10
  import { TeamInvitationCrud } from "./crud/team-invitation";
10
11
  import { TeamMemberProfilesCrud } from "./crud/team-member-profiles";
11
12
  import { TeamMembershipsCrud } from "./crud/team-memberships";
12
- import { ProjectPermissionsCrud } from "./crud/project-permissions";
13
13
  import { TeamPermissionsCrud } from "./crud/team-permissions";
14
14
  import { TeamsCrud } from "./crud/teams";
15
15
  import { UsersCrud } from "./crud/users";
@@ -66,6 +66,7 @@ export declare class StackServerInterface extends StackClientInterface {
66
66
  listServerTeams(options?: {
67
67
  userId?: string;
68
68
  }): Promise<TeamsCrud['Server']['Read'][]>;
69
+ getServerTeam(teamId: string): Promise<TeamsCrud['Server']['Read']>;
69
70
  listServerTeamUsers(teamId: string): Promise<UsersCrud['Server']['Read'][]>;
70
71
  createServerTeam(data: TeamsCrud['Server']['Create']): Promise<TeamsCrud['Server']['Read']>;
71
72
  updateServerTeam(teamId: string, data: TeamsCrud['Server']['Update']): Promise<TeamsCrud['Server']['Read']>;
@@ -123,6 +123,10 @@ export class StackServerInterface extends StackClientInterface {
123
123
  const result = await response.json();
124
124
  return result.items;
125
125
  }
126
+ async getServerTeam(teamId) {
127
+ const response = await this.sendServerRequest(`/teams/${teamId}`, {}, null);
128
+ return await response.json();
129
+ }
126
130
  async listServerTeamUsers(teamId) {
127
131
  const response = await this.sendServerRequest(`/users?team_id=${teamId}`, {}, null);
128
132
  const result = await response.json();
@@ -21,8 +21,8 @@ export declare const webhookEvents: readonly [{
21
21
  client_read_only_metadata?: {} | null | undefined;
22
22
  server_metadata?: {} | null | undefined;
23
23
  id: string;
24
- created_at_millis: number;
25
24
  display_name: string;
25
+ created_at_millis: number;
26
26
  profile_image_url: string | null;
27
27
  } | null;
28
28
  selected_team_id: string | null;
@@ -91,8 +91,8 @@ export declare const webhookEvents: readonly [{
91
91
  client_read_only_metadata?: {} | null | undefined;
92
92
  server_metadata?: {} | null | undefined;
93
93
  id: string;
94
- created_at_millis: number;
95
94
  display_name: string;
95
+ created_at_millis: number;
96
96
  profile_image_url: string | null;
97
97
  } | null;
98
98
  selected_team_id: string | null;
@@ -235,7 +235,9 @@ export declare const KnownErrors: {
235
235
  UserNotFound: KnownErrorConstructor<KnownError & KnownErrorBrand<"USER_NOT_FOUND">, []> & {
236
236
  errorCode: "USER_NOT_FOUND";
237
237
  };
238
- ApiKeyNotFound: KnownErrorConstructor<KnownError & KnownErrorBrand<"API_KEY_NOT_FOUND">, []> & {
238
+ ApiKeyNotFound: KnownErrorConstructor<KnownError & KnownErrorBrand<"API_KEY_NOT_VALID"> & {
239
+ constructorArgs: [statusCode: number, humanReadableMessage: string, details?: Json | undefined];
240
+ } & KnownErrorBrand<"API_KEY_NOT_FOUND">, []> & {
239
241
  errorCode: "API_KEY_NOT_FOUND";
240
242
  };
241
243
  ProjectNotFound: KnownErrorConstructor<KnownError & KnownErrorBrand<"PROJECT_NOT_FOUND">, [projectId: string]> & {
@@ -403,5 +405,23 @@ export declare const KnownErrors: {
403
405
  InvalidPollingCodeError: KnownErrorConstructor<KnownError & KnownErrorBrand<"INVALID_POLLING_CODE">, [details?: Json | undefined]> & {
404
406
  errorCode: "INVALID_POLLING_CODE";
405
407
  };
408
+ ApiKeyNotValid: KnownErrorConstructor<KnownError & KnownErrorBrand<"API_KEY_NOT_VALID">, [statusCode: number, humanReadableMessage: string, details?: Json | undefined]> & {
409
+ errorCode: "API_KEY_NOT_VALID";
410
+ };
411
+ ApiKeyExpired: KnownErrorConstructor<KnownError & KnownErrorBrand<"API_KEY_NOT_VALID"> & {
412
+ constructorArgs: [statusCode: number, humanReadableMessage: string, details?: Json | undefined];
413
+ } & KnownErrorBrand<"API_KEY_EXPIRED">, []> & {
414
+ errorCode: "API_KEY_EXPIRED";
415
+ };
416
+ ApiKeyRevoked: KnownErrorConstructor<KnownError & KnownErrorBrand<"API_KEY_NOT_VALID"> & {
417
+ constructorArgs: [statusCode: number, humanReadableMessage: string, details?: Json | undefined];
418
+ } & KnownErrorBrand<"API_KEY_REVOKED">, []> & {
419
+ errorCode: "API_KEY_REVOKED";
420
+ };
421
+ WrongApiKeyType: KnownErrorConstructor<KnownError & KnownErrorBrand<"API_KEY_NOT_VALID"> & {
422
+ constructorArgs: [statusCode: number, humanReadableMessage: string, details?: Json | undefined];
423
+ } & KnownErrorBrand<"WRONG_API_KEY_TYPE">, [expectedType: string, actualType: string]> & {
424
+ errorCode: "WRONG_API_KEY_TYPE";
425
+ };
406
426
  };
407
427
  export {};
@@ -293,10 +293,6 @@ const UserNotFound = createKnownErrorConstructor(KnownError, "USER_NOT_FOUND", (
293
293
  404,
294
294
  "User not found.",
295
295
  ], () => []);
296
- const ApiKeyNotFound = createKnownErrorConstructor(KnownError, "API_KEY_NOT_FOUND", () => [
297
- 404,
298
- "API key not found.",
299
- ], () => []);
300
296
  const ProjectNotFound = createKnownErrorConstructor(KnownError, "PROJECT_NOT_FOUND", (projectId) => {
301
297
  if (typeof projectId !== "string")
302
298
  throw new StackAssertionError("projectId of KnownErrors.ProjectNotFound must be a string");
@@ -570,6 +566,24 @@ const InvalidPollingCodeError = createKnownErrorConstructor(KnownError, "INVALID
570
566
  "The polling code is invalid or does not exist.",
571
567
  details,
572
568
  ], (json) => [json]);
569
+ const ApiKeyNotValid = createKnownErrorConstructor(KnownError, "API_KEY_NOT_VALID", "inherit", "inherit");
570
+ const ApiKeyExpired = createKnownErrorConstructor(ApiKeyNotValid, "API_KEY_EXPIRED", () => [
571
+ 401,
572
+ "API key has expired.",
573
+ ], () => []);
574
+ const ApiKeyRevoked = createKnownErrorConstructor(ApiKeyNotValid, "API_KEY_REVOKED", () => [
575
+ 401,
576
+ "API key has been revoked.",
577
+ ], () => []);
578
+ const WrongApiKeyType = createKnownErrorConstructor(ApiKeyNotValid, "WRONG_API_KEY_TYPE", (expectedType, actualType) => [
579
+ 400,
580
+ `This endpoint is for ${expectedType} API keys, but a ${actualType} API key was provided.`,
581
+ { expected_type: expectedType, actual_type: actualType },
582
+ ], (json) => [json.expected_type, json.actual_type]);
583
+ const ApiKeyNotFound = createKnownErrorConstructor(ApiKeyNotValid, "API_KEY_NOT_FOUND", () => [
584
+ 404,
585
+ "API key not found.",
586
+ ], () => []);
573
587
  const PermissionIdAlreadyExists = createKnownErrorConstructor(KnownError, "PERMISSION_ID_ALREADY_EXISTS", (permissionId) => [
574
588
  400,
575
589
  `Permission with ID "${permissionId}" already exists. Choose a different ID.`,
@@ -673,6 +687,10 @@ export const KnownErrors = {
673
687
  OAuthProviderAccessDenied,
674
688
  ContactChannelAlreadyUsedForAuthBySomeoneElse,
675
689
  InvalidPollingCodeError,
690
+ ApiKeyNotValid,
691
+ ApiKeyExpired,
692
+ ApiKeyRevoked,
693
+ WrongApiKeyType,
676
694
  };
677
695
  // ensure that all known error codes are unique
678
696
  const knownErrorCodes = new Set();
@@ -130,7 +130,7 @@ export declare const signInResponseSchema: yup.ObjectSchema<{
130
130
  is_new_user: undefined;
131
131
  user_id: undefined;
132
132
  }, "">;
133
- export declare const teamSystemPermissions: readonly ["$update_team", "$delete_team", "$read_members", "$remove_members", "$invite_members"];
133
+ export declare const teamSystemPermissions: readonly ["$update_team", "$delete_team", "$read_members", "$remove_members", "$invite_members", "$manage_api_keys"];
134
134
  export declare const permissionDefinitionIdSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
135
135
  export declare const customPermissionDefinitionIdSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
136
136
  export declare const teamPermissionDescriptionSchema: yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
@@ -147,7 +147,7 @@ export function yupUnion(...args) {
147
147
  const errors = [];
148
148
  for (const schema of args) {
149
149
  try {
150
- await schema.validate(value, context.options);
150
+ await yupValidate(schema, value, context.options);
151
151
  return true;
152
152
  }
153
153
  catch (e) {
@@ -326,6 +326,7 @@ export const teamSystemPermissions = [
326
326
  '$read_members',
327
327
  '$remove_members',
328
328
  '$invite_members',
329
+ '$manage_api_keys',
329
330
  ];
330
331
  export const permissionDefinitionIdSchema = yupString()
331
332
  .matches(/^\$?[a-z0-9_:]+$/, 'Only lowercase letters, numbers, ":", "_" and optional "$" at the beginning are allowed')
@@ -0,0 +1,23 @@
1
+ /**
2
+ * An api key has the following format:
3
+ * <prefix_without_underscores>_<secret_part_45_chars><id_part_32_chars><type_user_or_team_4_chars><scanner_and_marker_10_chars><checksum_8_chars>
4
+ *
5
+ * The scanner and marker is a base32 character that is used to determine if the api key is a public or private key
6
+ * and if it is a cloud or self-hosted key.
7
+ *
8
+ * The checksum is a crc32 checksum of the api key encoded in hex.
9
+ *
10
+ */
11
+ type ProjectApiKey = {
12
+ id: string;
13
+ prefix: string;
14
+ isPublic: boolean;
15
+ isCloudVersion: boolean;
16
+ secret: string;
17
+ checksum: string;
18
+ type: "user" | "team";
19
+ };
20
+ export declare function isApiKey(secret: string): boolean;
21
+ export declare function createProjectApiKey(options: Pick<ProjectApiKey, "id" | "isPublic" | "isCloudVersion" | "type">): string;
22
+ export declare function parseProjectApiKey(secret: string): ProjectApiKey;
23
+ export {};
@@ -0,0 +1,75 @@
1
+ import { getBase32CharacterFromIndex } from "@stackframe/stack-shared/dist/utils/bytes";
2
+ import { generateSecureRandomString } from "@stackframe/stack-shared/dist/utils/crypto";
3
+ import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
4
+ import crc32 from 'crc/crc32';
5
+ const STACK_AUTH_MARKER = "574ck4u7h";
6
+ // API key part lengths
7
+ const API_KEY_LENGTHS = {
8
+ SECRET_PART: 45,
9
+ ID_PART: 32,
10
+ TYPE_PART: 4,
11
+ SCANNER_AND_MARKER: 10,
12
+ CHECKSUM: 8,
13
+ };
14
+ function createChecksumSync(checksummablePart) {
15
+ const data = new TextEncoder().encode(checksummablePart);
16
+ const calculated_checksum = crc32(data);
17
+ return calculated_checksum.toString(16).padStart(8, "0");
18
+ }
19
+ function createApiKeyParts(options) {
20
+ const { id, isPublic, isCloudVersion, type } = options;
21
+ const prefix = isPublic ? "pk" : "sk";
22
+ const scannerFlag = (isCloudVersion ? 0 : 1) + (isPublic ? 2 : 0) + ( /* version */0);
23
+ const secretPart = generateSecureRandomString();
24
+ const idPart = id.replace(/-/g, "");
25
+ const scannerAndMarker = getBase32CharacterFromIndex(scannerFlag).toLowerCase() + STACK_AUTH_MARKER;
26
+ const checksummablePart = `${prefix}_${secretPart}${idPart}${type}${scannerAndMarker}`;
27
+ return { checksummablePart, idPart, prefix, scannerAndMarker, type };
28
+ }
29
+ function parseApiKeyParts(secret) {
30
+ const regex = new RegExp(`^([^_]+)_` + // prefix
31
+ `(.{${API_KEY_LENGTHS.SECRET_PART}})` + // secretPart
32
+ `(.{${API_KEY_LENGTHS.ID_PART}})` + // idPart
33
+ `(.{${API_KEY_LENGTHS.TYPE_PART}})` + // type
34
+ `(.{${API_KEY_LENGTHS.SCANNER_AND_MARKER}})` + // scannerAndMarker
35
+ `(.{${API_KEY_LENGTHS.CHECKSUM}})$` // checksum
36
+ );
37
+ const match = secret.match(regex);
38
+ if (!match) {
39
+ throw new StackAssertionError("Invalid API key format");
40
+ }
41
+ const [, prefix, secretPart, idPart, type, scannerAndMarker, checksum] = match;
42
+ const scannerFlag = scannerAndMarker.replace(STACK_AUTH_MARKER, "");
43
+ const isCloudVersion = parseInt(scannerFlag, 32) % 2 === 0;
44
+ const isPublic = (parseInt(scannerFlag, 32) & 2) !== 0;
45
+ const checksummablePart = `${prefix}_${secretPart}${idPart}${type}${scannerAndMarker}`;
46
+ const restored_id = idPart.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5");
47
+ if (!["user", "team"].includes(type)) {
48
+ throw new StackAssertionError("Invalid type");
49
+ }
50
+ return { checksummablePart, checksum, id: restored_id, isCloudVersion, isPublic, prefix, type: type };
51
+ }
52
+ export function isApiKey(secret) {
53
+ return secret.includes("_") && secret.includes(STACK_AUTH_MARKER);
54
+ }
55
+ export function createProjectApiKey(options) {
56
+ const { checksummablePart } = createApiKeyParts(options);
57
+ const checksum = createChecksumSync(checksummablePart);
58
+ return `${checksummablePart}${checksum}`;
59
+ }
60
+ export function parseProjectApiKey(secret) {
61
+ const { checksummablePart, checksum, id, isCloudVersion, isPublic, prefix, type } = parseApiKeyParts(secret);
62
+ const calculated_checksum = createChecksumSync(checksummablePart);
63
+ if (calculated_checksum !== checksum) {
64
+ throw new StackAssertionError("Checksum mismatch");
65
+ }
66
+ return {
67
+ id,
68
+ prefix,
69
+ isPublic,
70
+ isCloudVersion,
71
+ secret,
72
+ checksum,
73
+ type,
74
+ };
75
+ }
@@ -1,3 +1,6 @@
1
+ export declare function toHexString(input: Uint8Array): string;
2
+ export declare function getBase32CharacterFromIndex(index: number): string;
3
+ export declare function getBase32IndexFromCharacter(character: string): number;
1
4
  export declare function encodeBase32(input: Uint8Array): string;
2
5
  export declare function decodeBase32(input: string): Uint8Array;
3
6
  export declare function encodeBase64(input: Uint8Array): string;
@@ -5,6 +5,43 @@ const crockfordReplacements = new Map([
5
5
  ["i", "1"],
6
6
  ["l", "1"],
7
7
  ]);
8
+ export function toHexString(input) {
9
+ return Array.from(input).map(b => b.toString(16).padStart(2, "0")).join("");
10
+ }
11
+ import.meta.vitest?.test("toHexString", ({ expect }) => {
12
+ expect(toHexString(new Uint8Array([]))).toBe("");
13
+ expect(toHexString(new Uint8Array([0]))).toBe("00");
14
+ expect(toHexString(new Uint8Array([15]))).toBe("0f");
15
+ expect(toHexString(new Uint8Array([16]))).toBe("10");
16
+ expect(toHexString(new Uint8Array([255]))).toBe("ff");
17
+ expect(toHexString(new Uint8Array([1, 2, 3]))).toBe("010203");
18
+ });
19
+ export function getBase32CharacterFromIndex(index) {
20
+ if (index < 0 || index >= crockfordAlphabet.length) {
21
+ throw new StackAssertionError(`Invalid base32 index: ${index}`);
22
+ }
23
+ return crockfordAlphabet[index];
24
+ }
25
+ import.meta.vitest?.test("getBase32CharacterFromIndex", ({ expect }) => {
26
+ expect(getBase32CharacterFromIndex(0)).toBe("0");
27
+ expect(getBase32CharacterFromIndex(15)).toBe("F");
28
+ expect(() => getBase32CharacterFromIndex(32)).toThrow();
29
+ });
30
+ export function getBase32IndexFromCharacter(character) {
31
+ if (character.length !== 1) {
32
+ throw new StackAssertionError(`Invalid base32 character: ${character}`);
33
+ }
34
+ const index = crockfordAlphabet.indexOf(character.toUpperCase());
35
+ if (index === -1) {
36
+ throw new StackAssertionError(`Invalid base32 character: ${character}`);
37
+ }
38
+ return index;
39
+ }
40
+ import.meta.vitest?.test("getBase32IndexFromCharacter", ({ expect }) => {
41
+ expect(getBase32IndexFromCharacter("0")).toBe(0);
42
+ expect(getBase32IndexFromCharacter("F")).toBe(15);
43
+ expect(() => getBase32IndexFromCharacter("_")).toThrow();
44
+ });
8
45
  export function encodeBase32(input) {
9
46
  let bits = 0;
10
47
  let value = 0;
@@ -13,12 +50,12 @@ export function encodeBase32(input) {
13
50
  value = (value << 8) | input[i];
14
51
  bits += 8;
15
52
  while (bits >= 5) {
16
- output += crockfordAlphabet[(value >>> (bits - 5)) & 31];
53
+ output += getBase32CharacterFromIndex((value >>> (bits - 5)) & 31);
17
54
  bits -= 5;
18
55
  }
19
56
  }
20
57
  if (bits > 0) {
21
- output += crockfordAlphabet[(value << (5 - bits)) & 31];
58
+ output += getBase32CharacterFromIndex((value << (5 - bits)) & 31);
22
59
  }
23
60
  // sanity check
24
61
  if (!isBase32(output)) {
@@ -26,6 +63,14 @@ export function encodeBase32(input) {
26
63
  }
27
64
  return output;
28
65
  }
66
+ import.meta.vitest?.test("encodeBase32", ({ expect }) => {
67
+ expect(encodeBase32(new Uint8Array([]))).toBe("");
68
+ expect(encodeBase32(new Uint8Array([1]))).toBe("04");
69
+ expect(encodeBase32(new Uint8Array([15]))).toBe("1W");
70
+ expect(encodeBase32(new Uint8Array([16]))).toBe("20");
71
+ expect(encodeBase32(new Uint8Array([255]))).toBe("ZW");
72
+ expect(encodeBase32(new Uint8Array([255, 255]))).toBe("ZZZG");
73
+ });
29
74
  export function decodeBase32(input) {
30
75
  if (!isBase32(input)) {
31
76
  throw new StackAssertionError("Invalid base32 string");
@@ -41,10 +86,7 @@ export function decodeBase32(input) {
41
86
  if (crockfordReplacements.has(char)) {
42
87
  char = crockfordReplacements.get(char);
43
88
  }
44
- const index = crockfordAlphabet.indexOf(char);
45
- if (index === -1) {
46
- throw new Error(`Invalid character: ${char}`);
47
- }
89
+ const index = getBase32IndexFromCharacter(char);
48
90
  value = (value << 5) | index;
49
91
  bits += 5;
50
92
  if (bits >= 8) {
@@ -54,6 +96,13 @@ export function decodeBase32(input) {
54
96
  }
55
97
  return output;
56
98
  }
99
+ import.meta.vitest?.test("decodeBase32", ({ expect }) => {
100
+ expect(decodeBase32("")).toEqual(new Uint8Array([]));
101
+ expect(decodeBase32("00")).toEqual(new Uint8Array([0]));
102
+ expect(decodeBase32("1W")).toEqual(new Uint8Array([15]));
103
+ expect(decodeBase32("20")).toEqual(new Uint8Array([16]));
104
+ expect(decodeBase32("ZW")).toEqual(new Uint8Array([255]));
105
+ });
57
106
  export function encodeBase64(input) {
58
107
  const res = btoa(String.fromCharCode(...input));
59
108
  // Skip sanity check for test cases
@@ -1,4 +1,4 @@
1
- export declare function sha512(input: Uint8Array | string): Promise<string>;
1
+ export declare function sha512(input: Uint8Array | string): Promise<Uint8Array>;
2
2
  export declare function hashPassword(password: string): Promise<string>;
3
3
  export declare function comparePassword(password: string, hash: string): Promise<boolean>;
4
4
  export declare function isPasswordHashValid(hash: string): Promise<boolean>;
@@ -2,9 +2,7 @@ import bcrypt from 'bcrypt';
2
2
  import { StackAssertionError } from './errors';
3
3
  export async function sha512(input) {
4
4
  const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
5
- return await crypto.subtle.digest("SHA-512", bytes).then(buf => {
6
- return Array.prototype.map.call(new Uint8Array(buf), x => (('00' + x.toString(16)).slice(-2))).join('');
7
- });
5
+ return new Uint8Array(await crypto.subtle.digest("SHA-512", bytes));
8
6
  }
9
7
  export async function hashPassword(password) {
10
8
  const passwordBytes = new TextEncoder().encode(password);
@@ -26,6 +26,14 @@ export type FilterUndefined<T> = {
26
26
  * TypeScript's `Partial<XYZ>` type allows `undefined` values.
27
27
  */
28
28
  export declare function filterUndefined<T extends {}>(obj: T): FilterUndefined<T>;
29
+ export type FilterUndefinedOrNull<T> = FilterUndefined<{
30
+ [k in keyof T]: null extends T[k] ? NonNullable<T[k]> | undefined : T[k];
31
+ }>;
32
+ /**
33
+ * Returns a new object with all undefined and null values removed. Useful when spreading optional parameters on an object, as
34
+ * TypeScript's `Partial<XYZ>` type allows `undefined` values.
35
+ */
36
+ export declare function filterUndefinedOrNull<T extends {}>(obj: T): FilterUndefinedOrNull<T>;
29
37
  export declare function pick<T extends {}, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>;
30
38
  export declare function omit<T extends {}, K extends keyof T>(obj: T, keys: K[]): Omit<T, K>;
31
39
  export declare function split<T extends {}, K extends keyof T>(obj: T, keys: K[]): [Pick<T, K>, Omit<T, K>];
@@ -215,6 +215,17 @@ import.meta.vitest?.test("filterUndefined", ({ expect }) => {
215
215
  expect(filterUndefined({ a: null, b: undefined })).toEqual({ a: null });
216
216
  expect(filterUndefined({ a: 0, b: "", c: false, d: undefined })).toEqual({ a: 0, b: "", c: false });
217
217
  });
218
+ /**
219
+ * Returns a new object with all undefined and null values removed. Useful when spreading optional parameters on an object, as
220
+ * TypeScript's `Partial<XYZ>` type allows `undefined` values.
221
+ */
222
+ export function filterUndefinedOrNull(obj) {
223
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null));
224
+ }
225
+ import.meta.vitest?.test("filterUndefinedOrNull", ({ expect }) => {
226
+ expect(filterUndefinedOrNull({})).toEqual({});
227
+ expect(filterUndefinedOrNull({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 });
228
+ });
218
229
  export function pick(obj, keys) {
219
230
  return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.includes(k)));
220
231
  }