@fonoster/identity 0.26.0 → 0.30.0

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 (119) hide show
  1. package/README.md +25 -0
  2. package/dist/allowList.d.ts +4 -0
  3. package/dist/allowList.js +7 -1
  4. package/dist/apikeys/createCreateApiKey.js +16 -0
  5. package/dist/apikeys/createDeleteApiKey.js +22 -2
  6. package/dist/apikeys/createListApiKeys.js +14 -0
  7. package/dist/apikeys/createRegenerateApiKey.js +23 -2
  8. package/dist/config/index.d.ts +20 -0
  9. package/dist/config/index.js +36 -0
  10. package/dist/config/publishedKeys.d.ts +9 -0
  11. package/dist/config/publishedKeys.js +56 -0
  12. package/dist/config/schemas.d.ts +392 -0
  13. package/dist/config/schemas.js +163 -0
  14. package/dist/config/toIdentityConfig.d.ts +19 -0
  15. package/dist/config/toIdentityConfig.js +93 -0
  16. package/dist/constants.d.ts +28 -0
  17. package/dist/constants.js +29 -1
  18. package/dist/exchanges/createExchangeApiKey.js +4 -1
  19. package/dist/exchanges/createExchangeCredentials.d.ts +6 -1
  20. package/dist/exchanges/createExchangeCredentials.js +20 -2
  21. package/dist/exchanges/createExchangeOauth2Code.js +25 -2
  22. package/dist/exchanges/createExchangeRefreshToken.d.ts +9 -1
  23. package/dist/exchanges/createExchangeRefreshToken.js +57 -13
  24. package/dist/exchanges/createExchangeVerificationCode.d.ts +35 -0
  25. package/dist/exchanges/createExchangeVerificationCode.js +95 -0
  26. package/dist/exchanges/createRevokeToken.d.ts +33 -0
  27. package/dist/exchanges/createRevokeToken.js +64 -0
  28. package/dist/exchanges/exchangeTokens.d.ts +3 -1
  29. package/dist/exchanges/exchangeTokens.js +23 -5
  30. package/dist/exchanges/index.d.ts +2 -0
  31. package/dist/exchanges/index.js +2 -0
  32. package/dist/exchanges/payloads/users/createGetIdTokenPayload.js +3 -2
  33. package/dist/exchanges/refreshTokens.d.ts +30 -0
  34. package/dist/exchanges/refreshTokens.js +101 -0
  35. package/dist/exchanges/types.d.ts +53 -2
  36. package/dist/generated/@prisma/client/edge.js +32 -9
  37. package/dist/generated/@prisma/client/index-browser.js +24 -1
  38. package/dist/generated/@prisma/client/index.d.ts +3233 -417
  39. package/dist/generated/@prisma/client/index.js +34 -11
  40. package/dist/generated/@prisma/client/{libquery_engine-darwin-arm64.dylib.node → libquery_engine-debian-openssl-3.0.x.so.node} +0 -0
  41. package/dist/generated/@prisma/client/package.json +1 -1
  42. package/dist/generated/@prisma/client/schema.prisma +47 -7
  43. package/dist/generated/@prisma/client/wasm.js +32 -9
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/server/config.d.ts +286 -55
  47. package/dist/server/config.js +74 -108
  48. package/dist/server/index.js +5 -3
  49. package/dist/service.d.ts +6 -0
  50. package/dist/service.js +11 -4
  51. package/dist/users/createCreateUser.d.ts +6 -1
  52. package/dist/users/createCreateUser.js +17 -9
  53. package/dist/users/createCreateUserWithOauth2Code.js +19 -15
  54. package/dist/users/createResetPassword.d.ts +5 -0
  55. package/dist/users/createResetPassword.js +33 -7
  56. package/dist/users/createSendResetPasswordCode.d.ts +6 -0
  57. package/dist/users/createSendResetPasswordCode.js +33 -1
  58. package/dist/users/createUpdateUser.d.ts +7 -0
  59. package/dist/users/createUpdateUser.js +41 -8
  60. package/dist/users/createUserFromGitHub.d.ts +40 -0
  61. package/dist/users/createUserFromGitHub.js +27 -0
  62. package/dist/users/index.d.ts +1 -0
  63. package/dist/users/index.js +1 -0
  64. package/dist/users/migratePasswordStorage.d.ts +7 -0
  65. package/dist/users/migratePasswordStorage.js +47 -0
  66. package/dist/users/types.d.ts +3 -1
  67. package/dist/users/upsertDefaultUser.js +6 -14
  68. package/dist/utils/authMethods.d.ts +31 -0
  69. package/dist/utils/authMethods.js +44 -0
  70. package/dist/utils/claimUnverifiedAccount.d.ts +35 -0
  71. package/dist/utils/claimUnverifiedAccount.js +43 -0
  72. package/dist/utils/clientAddress.d.ts +26 -0
  73. package/dist/utils/clientAddress.js +9 -0
  74. package/dist/utils/createGenerateVerificationCode.d.ts +4 -0
  75. package/dist/utils/createGenerateVerificationCode.js +12 -2
  76. package/dist/utils/createGenerateWorkspaceInviteToken.js +3 -0
  77. package/dist/utils/createGetApiKeyByAccessKeyId.d.ts +5 -1
  78. package/dist/utils/createGetApiKeyByAccessKeyId.js +5 -0
  79. package/dist/utils/createGetUserByAccessKeyId.d.ts +3 -1
  80. package/dist/utils/createGetUserByEmail.d.ts +3 -1
  81. package/dist/utils/createGetUserByGitHubEmails.d.ts +41 -0
  82. package/dist/utils/createGetUserByGitHubEmails.js +31 -0
  83. package/dist/utils/createGetUserByWorkspaceAccessKeyId.d.ts +3 -1
  84. package/dist/utils/createIsValidVerificationCode.d.ts +5 -0
  85. package/dist/utils/createIsValidVerificationCode.js +34 -7
  86. package/dist/utils/createSendEmail.d.ts +2 -1
  87. package/dist/utils/createSendEmail.js +17 -0
  88. package/dist/utils/createUpdateMembershipStatus.js +10 -3
  89. package/dist/utils/generateOneTimePassword.d.ts +3 -0
  90. package/dist/utils/generateOneTimePassword.js +25 -0
  91. package/dist/utils/getGitHubUserWithOauth2Code.d.ts +20 -2
  92. package/dist/utils/getGitHubUserWithOauth2Code.js +41 -9
  93. package/dist/utils/isWorkspaceAdminCall.d.ts +8 -0
  94. package/dist/utils/isWorkspaceAdminCall.js +47 -0
  95. package/dist/utils/normalizeContact.d.ts +22 -0
  96. package/dist/utils/normalizeContact.js +28 -0
  97. package/dist/utils/passwords.d.ts +11 -0
  98. package/dist/utils/passwords.js +84 -0
  99. package/dist/utils/rateLimiter.d.ts +20 -0
  100. package/dist/utils/rateLimiter.js +108 -0
  101. package/dist/utils/signInThrottle.d.ts +47 -0
  102. package/dist/utils/signInThrottle.js +90 -0
  103. package/dist/utils/userPasswords.d.ts +25 -0
  104. package/dist/utils/userPasswords.js +98 -0
  105. package/dist/verification/createSendVerificationCode.js +27 -1
  106. package/dist/verification/createVerifyCode.js +10 -0
  107. package/dist/workspaces/createCreateWorkspace.d.ts +7 -1
  108. package/dist/workspaces/createCreateWorkspace.js +41 -5
  109. package/dist/workspaces/createDeleteWorkspace.d.ts +2 -1
  110. package/dist/workspaces/createDeleteWorkspace.js +16 -2
  111. package/dist/workspaces/createInviteUserToWorkspace.js +19 -17
  112. package/dist/workspaces/createResendWorkspaceMembershipInvitation.js +17 -2
  113. package/dist/workspaces/createUpdateWorkspace.js +18 -4
  114. package/dist/workspaces/createWorkspaceDirectory.d.ts +29 -0
  115. package/dist/workspaces/createWorkspaceDirectory.js +42 -0
  116. package/dist/workspaces/index.d.ts +1 -0
  117. package/dist/workspaces/index.js +1 -0
  118. package/package.json +7 -6
  119. package/dist/generated/@prisma/client/runtime/wasm.js +0 -35
@@ -1,5 +1,10 @@
1
1
  import { Prisma } from "../db";
2
2
  import { VerificationType } from "../generated/@prisma/client";
3
+ /**
4
+ * Checks a code and, when it matches, consumes it: a code works once, only
5
+ * before it expires, and only until VERIFICATION_CODE_MAX_ATTEMPTS wrong
6
+ * guesses have been made against it.
7
+ */
3
8
  declare function createIsValidVerificationCode(prisma: Prisma): (params: {
4
9
  type: VerificationType;
5
10
  value: string;
@@ -29,24 +29,51 @@ exports.createIsValidVerificationCode = createIsValidVerificationCode;
29
29
  * limitations under the License.
30
30
  */
31
31
  const constants_1 = require("../constants");
32
+ const normalizeContact_1 = require("./normalizeContact");
33
+ const passwords_1 = require("./passwords");
34
+ /**
35
+ * Checks a code and, when it matches, consumes it: a code works once, only
36
+ * before it expires, and only until VERIFICATION_CODE_MAX_ATTEMPTS wrong
37
+ * guesses have been made against it.
38
+ */
32
39
  function createIsValidVerificationCode(prisma) {
33
40
  return (params) => __awaiter(this, void 0, void 0, function* () {
34
- const { type, value, code } = params;
35
- // Delete old verification codes
41
+ const { type, code } = params;
42
+ const value = (0, normalizeContact_1.normalizeContact)(type, params.value);
43
+ const now = new Date();
44
+ // Housekeeping: drop codes that expired a while ago.
36
45
  yield prisma.verificationCode.deleteMany({
37
46
  where: {
38
47
  expiresAt: {
39
- lt: new Date(Date.now() - constants_1.VERIFICATION_CODE_EXPIRATION)
48
+ lt: new Date(now.getTime() - constants_1.VERIFICATION_CODE_EXPIRATION)
40
49
  }
41
50
  }
42
51
  });
43
- const result = yield prisma.verificationCode.findFirst({
52
+ const current = yield prisma.verificationCode.findFirst({
44
53
  where: {
45
54
  type,
46
55
  value,
47
- code
48
- }
56
+ consumedAt: null,
57
+ expiresAt: { gt: now },
58
+ attempts: { lt: constants_1.VERIFICATION_CODE_MAX_ATTEMPTS }
59
+ },
60
+ orderBy: { createdAt: "desc" }
61
+ });
62
+ if (!current) {
63
+ return false;
64
+ }
65
+ if (!code || !(0, passwords_1.equalsInConstantTime)(current.code, code)) {
66
+ yield prisma.verificationCode.update({
67
+ where: { ref: current.ref },
68
+ data: { attempts: { increment: 1 } }
69
+ });
70
+ return false;
71
+ }
72
+ // Conditional update so two concurrent requests can't both use the code.
73
+ const { count } = yield prisma.verificationCode.updateMany({
74
+ where: { ref: current.ref, consumedAt: null },
75
+ data: { consumedAt: now }
49
76
  });
50
- return !!result;
77
+ return count === 1;
51
78
  });
52
79
  }
@@ -1,3 +1,4 @@
1
1
  import { IdentityConfig } from "../exchanges/types";
2
+ declare const SMTP_NOT_CONFIGURED = "email can't be sent: smtp is not configured (set smtp in the config file)";
2
3
  declare function createSendEmail(identityConfig: IdentityConfig): (params: import("@fonoster/common").EmailParams) => Promise<void>;
3
- export { createSendEmail };
4
+ export { createSendEmail, SMTP_NOT_CONFIGURED };
@@ -1,5 +1,15 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SMTP_NOT_CONFIGURED = void 0;
3
13
  exports.createSendEmail = createSendEmail;
4
14
  /**
5
15
  * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
@@ -20,8 +30,15 @@ exports.createSendEmail = createSendEmail;
20
30
  * limitations under the License.
21
31
  */
22
32
  const common_1 = require("@fonoster/common");
33
+ const SMTP_NOT_CONFIGURED = "email can't be sent: smtp is not configured (set smtp in the config file)";
34
+ exports.SMTP_NOT_CONFIGURED = SMTP_NOT_CONFIGURED;
23
35
  function createSendEmail(identityConfig) {
24
36
  const { smtpConfig } = identityConfig;
37
+ if (!smtpConfig) {
38
+ return () => __awaiter(this, void 0, void 0, function* () {
39
+ throw new Error(SMTP_NOT_CONFIGURED);
40
+ });
41
+ }
25
42
  const { host, port, secure, sender, auth } = smtpConfig;
26
43
  const { user, pass } = auth;
27
44
  return (0, common_1.createSendEmail)({
@@ -30,16 +30,23 @@ exports.createUpdateMembershipStatus = createUpdateMembershipStatus;
30
30
  */
31
31
  const common_1 = require("@fonoster/common");
32
32
  const types_1 = require("@fonoster/types");
33
- const jwt_decode_1 = require("jwt-decode");
34
33
  const db_1 = require("../db");
35
34
  function createUpdateMembershipStatus(identityConfig) {
36
35
  const prisma = (0, db_1.createPrismaClient)(identityConfig.dbUrl, identityConfig.encryptionKey);
37
36
  return function pdateMembershipStatus(token) {
38
37
  return __awaiter(this, void 0, void 0, function* () {
39
- if (!(0, common_1.isValidToken)(token, identityConfig.privateKey)) {
38
+ const claims = (0, common_1.verifyToken)(token, identityConfig.privateKey, {
39
+ issuer: identityConfig.issuer,
40
+ audience: identityConfig.audience
41
+ });
42
+ // Invite tokens only (older ones carry no tokenUse); never a session token.
43
+ if (!claims ||
44
+ (claims.tokenUse !== undefined &&
45
+ claims.tokenUse !== common_1.TokenUseEnum.INVITE) ||
46
+ typeof claims.memberRef !== "string") {
40
47
  throw new Error("Invalid token");
41
48
  }
42
- const { memberRef } = (0, jwt_decode_1.jwtDecode)(token);
49
+ const { memberRef } = claims;
43
50
  yield prisma.workspaceMember.update({
44
51
  where: {
45
52
  ref: memberRef
@@ -0,0 +1,3 @@
1
+ /** The 10-character password emailed with an invite. */
2
+ declare const generateOneTimePassword: (size?: number) => string;
3
+ export { generateOneTimePassword };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateOneTimePassword = void 0;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const nanoid_1 = require("nanoid");
23
+ /** The 10-character password emailed with an invite. */
24
+ const generateOneTimePassword = (0, nanoid_1.customAlphabet)("23456789abcdefghjkmnpqrstuvwxyz", 10);
25
+ exports.generateOneTimePassword = generateOneTimePassword;
@@ -1,6 +1,24 @@
1
+ type GitHubUser = {
2
+ name?: string;
3
+ login?: string;
4
+ avatar_url?: string;
5
+ /** The primary verified address, or undefined when there is none. */
6
+ email?: string;
7
+ /** Every verified address, primary first; used to find an existing user. */
8
+ verifiedEmails?: string[];
9
+ };
10
+ /**
11
+ * Trades an OAuth code for the GitHub user. The email is the account's
12
+ * primary *verified* address from /user/emails, never the public profile
13
+ * email, which the user can set to anything.
14
+ */
1
15
  declare function getGitHubUserWithOauth2Code(params: {
2
16
  clientId: string;
3
17
  clientSecret: string;
4
18
  code: string;
5
- }): Promise<any>;
6
- export { getGitHubUserWithOauth2Code };
19
+ /** Defaults to https://github.com; a fake GitHub in tests. */
20
+ url?: string;
21
+ /** Defaults to https://api.github.com. */
22
+ apiUrl?: string;
23
+ }): Promise<GitHubUser>;
24
+ export { getGitHubUserWithOauth2Code, GitHubUser };
@@ -28,12 +28,20 @@ exports.getGitHubUserWithOauth2Code = getGitHubUserWithOauth2Code;
28
28
  * See the License for the specific language governing permissions and
29
29
  * limitations under the License.
30
30
  */
31
- const GITHUB_API_URL = "https://github.com/login/oauth/access_token";
32
- const GITHUB_USER_API_URL = "https://api.github.com/user";
31
+ const GITHUB_URL = "https://github.com";
32
+ const GITHUB_API_URL = "https://api.github.com";
33
+ /**
34
+ * Trades an OAuth code for the GitHub user. The email is the account's
35
+ * primary *verified* address from /user/emails, never the public profile
36
+ * email, which the user can set to anything.
37
+ */
33
38
  function getGitHubUserWithOauth2Code(params) {
34
39
  return __awaiter(this, void 0, void 0, function* () {
40
+ var _a, _b;
35
41
  const { clientId, clientSecret, code } = params;
36
- const tokenResponse = yield fetch(GITHUB_API_URL, {
42
+ const url = (_a = params.url) !== null && _a !== void 0 ? _a : GITHUB_URL;
43
+ const apiUrl = (_b = params.apiUrl) !== null && _b !== void 0 ? _b : GITHUB_API_URL;
44
+ const tokenResponse = yield fetch(`${url}/login/oauth/access_token`, {
37
45
  method: "POST",
38
46
  headers: {
39
47
  "Content-Type": "application/json",
@@ -47,11 +55,35 @@ function getGitHubUserWithOauth2Code(params) {
47
55
  });
48
56
  const tokenData = yield tokenResponse.json();
49
57
  const accessToken = tokenData === null || tokenData === void 0 ? void 0 : tokenData.access_token;
50
- const userResponse = yield fetch(GITHUB_USER_API_URL, {
51
- headers: {
52
- Authorization: `token ${accessToken}`
53
- }
54
- });
55
- return userResponse.json();
58
+ if (!accessToken) {
59
+ return {};
60
+ }
61
+ const headers = {
62
+ Authorization: `token ${accessToken}`,
63
+ Accept: "application/vnd.github+json"
64
+ };
65
+ const [userResponse, emailsResponse] = yield Promise.all([
66
+ fetch(`${apiUrl}/user`, { headers }),
67
+ fetch(`${apiUrl}/user/emails`, { headers })
68
+ ]);
69
+ const user = yield userResponse.json();
70
+ const emails = emailsResponse.ok
71
+ ? yield emailsResponse.json()
72
+ : [];
73
+ const primary = Array.isArray(emails)
74
+ ? emails.find((e) => e.primary && e.verified)
75
+ : undefined;
76
+ const verified = Array.isArray(emails)
77
+ ? emails.filter((e) => e.verified).map((e) => e.email)
78
+ : [];
79
+ return {
80
+ name: (user === null || user === void 0 ? void 0 : user.name) || (user === null || user === void 0 ? void 0 : user.login),
81
+ login: user === null || user === void 0 ? void 0 : user.login,
82
+ avatar_url: user === null || user === void 0 ? void 0 : user.avatar_url,
83
+ email: primary === null || primary === void 0 ? void 0 : primary.email,
84
+ verifiedEmails: primary
85
+ ? [primary.email, ...verified.filter((e) => e !== primary.email)]
86
+ : verified
87
+ };
56
88
  });
57
89
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Whether the caller is an owner or admin of the workspace the call names
3
+ * (its `accesskeyid` metadata). Only the role held in that workspace counts.
4
+ * The interceptor has already verified the token; this re-checks the role in
5
+ * the handler and answers false on anything unexpected.
6
+ */
7
+ declare function isWorkspaceAdminCall(call: unknown): boolean;
8
+ export { isWorkspaceAdminCall };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isWorkspaceAdminCall = isWorkspaceAdminCall;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const common_1 = require("@fonoster/common");
23
+ const types_1 = require("@fonoster/types");
24
+ const ADMIN_ROLES = [types_1.Role.WORKSPACE_OWNER, types_1.Role.WORKSPACE_ADMIN];
25
+ /**
26
+ * Whether the caller is an owner or admin of the workspace the call names
27
+ * (its `accesskeyid` metadata). Only the role held in that workspace counts.
28
+ * The interceptor has already verified the token; this re-checks the role in
29
+ * the handler and answers false on anything unexpected.
30
+ */
31
+ function isWorkspaceAdminCall(call) {
32
+ try {
33
+ const typedCall = call;
34
+ const accessKeyId = (0, common_1.getAccessKeyIdFromCall)(typedCall);
35
+ const token = (0, common_1.decodeToken)((0, common_1.getTokenFromCall)(typedCall));
36
+ if (!accessKeyId || (token === null || token === void 0 ? void 0 : token.tokenUse) !== common_1.TokenUseEnum.ACCESS) {
37
+ return false;
38
+ }
39
+ const entry = Array.isArray(token.access)
40
+ ? token.access.find((a) => (a === null || a === void 0 ? void 0 : a.accessKeyId) === accessKeyId)
41
+ : undefined;
42
+ return entry ? ADMIN_ROLES.includes(entry.role) : false;
43
+ }
44
+ catch (_a) {
45
+ return false;
46
+ }
47
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { VerificationType } from "../generated/@prisma/client";
20
+ /** Emails are matched case-insensitively; phone numbers as given. */
21
+ declare function normalizeContact(type: VerificationType, value: string): string;
22
+ export { normalizeContact };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeContact = normalizeContact;
4
+ /**
5
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
6
+ * http://github.com/fonoster/fonoster
7
+ *
8
+ * This file is part of Fonoster
9
+ *
10
+ * Licensed under the MIT License (the "License");
11
+ * you may not use this file except in compliance with
12
+ * the License. You may obtain a copy of the License at
13
+ *
14
+ * https://opensource.org/licenses/MIT
15
+ *
16
+ * Unless required by applicable law or agreed to in writing, software
17
+ * distributed under the License is distributed on an "AS IS" BASIS,
18
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19
+ * See the License for the specific language governing permissions and
20
+ * limitations under the License.
21
+ */
22
+ const client_1 = require("../generated/@prisma/client");
23
+ /** Emails are matched case-insensitively; phone numbers as given. */
24
+ function normalizeContact(type, value) {
25
+ return type === client_1.VerificationType.EMAIL
26
+ ? (value !== null && value !== void 0 ? value : "").trim().toLowerCase()
27
+ : value;
28
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * One-way digest for storing a password: `scrypt$N$r$p$salt$hash`
3
+ * (base64 salt and hash). The parameters travel with the digest so they can
4
+ * be raised later without invalidating existing ones.
5
+ */
6
+ declare function hashPassword(password: string): Promise<string>;
7
+ /** Checks a password against a digest from `hashPassword`, in constant time. */
8
+ declare function verifyPasswordDigest(password: string, digest: string): Promise<boolean>;
9
+ /** Constant-time comparison for legacy (decrypted) passwords. */
10
+ declare function equalsInConstantTime(a: string, b: string): boolean;
11
+ export { equalsInConstantTime, hashPassword, verifyPasswordDigest };
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.equalsInConstantTime = equalsInConstantTime;
13
+ exports.hashPassword = hashPassword;
14
+ exports.verifyPasswordDigest = verifyPasswordDigest;
15
+ /**
16
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
17
+ * http://github.com/fonoster/fonoster
18
+ *
19
+ * This file is part of Fonoster
20
+ *
21
+ * Licensed under the MIT License (the "License");
22
+ * you may not use this file except in compliance with
23
+ * the License. You may obtain a copy of the License at
24
+ *
25
+ * https://opensource.org/licenses/MIT
26
+ *
27
+ * Unless required by applicable law or agreed to in writing, software
28
+ * distributed under the License is distributed on an "AS IS" BASIS,
29
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30
+ * See the License for the specific language governing permissions and
31
+ * limitations under the License.
32
+ */
33
+ const crypto_1 = require("crypto");
34
+ const SCRYPT_PARAMS = { N: Math.pow(2, 15), r: 8, p: 1 };
35
+ const KEY_LENGTH = 64;
36
+ const SALT_LENGTH = 16;
37
+ function deriveKey(password, salt, params) {
38
+ const options = Object.assign(Object.assign({}, params), {
39
+ // scrypt needs 128 * N * r bytes; leave headroom over Node's 32 MiB default.
40
+ maxmem: 256 * params.N * params.r });
41
+ return new Promise((resolve, reject) => (0, crypto_1.scrypt)(password, salt, KEY_LENGTH, options, (error, key) => error ? reject(error) : resolve(key)));
42
+ }
43
+ /**
44
+ * One-way digest for storing a password: `scrypt$N$r$p$salt$hash`
45
+ * (base64 salt and hash). The parameters travel with the digest so they can
46
+ * be raised later without invalidating existing ones.
47
+ */
48
+ function hashPassword(password) {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ const salt = (0, crypto_1.randomBytes)(SALT_LENGTH);
51
+ const { N, r, p } = SCRYPT_PARAMS;
52
+ const key = yield deriveKey(password, salt, SCRYPT_PARAMS);
53
+ return [
54
+ "scrypt",
55
+ N,
56
+ r,
57
+ p,
58
+ salt.toString("base64"),
59
+ key.toString("base64")
60
+ ].join("$");
61
+ });
62
+ }
63
+ /** Checks a password against a digest from `hashPassword`, in constant time. */
64
+ function verifyPasswordDigest(password, digest) {
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ const [scheme, N, r, p, salt, hash] = digest.split("$");
67
+ if (scheme !== "scrypt" || !salt || !hash) {
68
+ return false;
69
+ }
70
+ const expected = Buffer.from(hash, "base64");
71
+ const actual = yield deriveKey(password, Buffer.from(salt, "base64"), {
72
+ N: Number(N),
73
+ r: Number(r),
74
+ p: Number(p)
75
+ });
76
+ return actual.length === expected.length && (0, crypto_1.timingSafeEqual)(actual, expected);
77
+ });
78
+ }
79
+ /** Constant-time comparison for legacy (decrypted) passwords. */
80
+ function equalsInConstantTime(a, b) {
81
+ const left = Buffer.from(a);
82
+ const right = Buffer.from(b);
83
+ return left.length === right.length && (0, crypto_1.timingSafeEqual)(left, right);
84
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Fixed-window counter kept in this process's memory. With several replicas each enforces its own
3
+ * limit, and a restart resets it: use createSharedRateLimiter in the service.
4
+ */
5
+ declare function createRateLimiter(params: {
6
+ limit: number;
7
+ windowMs: number;
8
+ }): (key: string, now?: number) => boolean;
9
+ /**
10
+ * The same fixed-window counter in the Identity database (table rate_limit_windows), so the limit
11
+ * holds across every replica and survives a restart (ST-4). One statement counts the use and
12
+ * starts a new window when the old one is over. `name` keeps the counters of different limits
13
+ * apart. If the database cannot be reached the use is counted in this process instead.
14
+ */
15
+ declare function createSharedRateLimiter(prisma: unknown, params: {
16
+ name: string;
17
+ limit: number;
18
+ windowMs: number;
19
+ }): (key: string) => Promise<boolean>;
20
+ export { createRateLimiter, createSharedRateLimiter };
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.createRateLimiter = createRateLimiter;
13
+ exports.createSharedRateLimiter = createSharedRateLimiter;
14
+ /**
15
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
16
+ * http://github.com/fonoster/fonoster
17
+ *
18
+ * This file is part of Fonoster
19
+ *
20
+ * Licensed under the MIT License (the "License");
21
+ * you may not use this file except in compliance with
22
+ * the License. You may obtain a copy of the License at
23
+ *
24
+ * https://opensource.org/licenses/MIT
25
+ *
26
+ * Unless required by applicable law or agreed to in writing, software
27
+ * distributed under the License is distributed on an "AS IS" BASIS,
28
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29
+ * See the License for the specific language governing permissions and
30
+ * limitations under the License.
31
+ */
32
+ const logger_1 = require("@fonoster/logger");
33
+ const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
34
+ /**
35
+ * Fixed-window counter kept in this process's memory. With several replicas each enforces its own
36
+ * limit, and a restart resets it: use createSharedRateLimiter in the service.
37
+ */
38
+ function createRateLimiter(params) {
39
+ const { limit, windowMs } = params;
40
+ const windows = new Map();
41
+ /** Counts one use of `key`; false when it's over the limit. */
42
+ return function tryConsume(key, now = Date.now()) {
43
+ const window = windows.get(key);
44
+ if (!window || now - window.startedAt >= windowMs) {
45
+ windows.set(key, { startedAt: now, count: 1 });
46
+ // Keep the map from growing without bound.
47
+ if (windows.size > 10000) {
48
+ windows.forEach((w, k) => {
49
+ if (now - w.startedAt >= windowMs)
50
+ windows.delete(k);
51
+ });
52
+ }
53
+ return true;
54
+ }
55
+ if (window.count >= limit) {
56
+ return false;
57
+ }
58
+ window.count += 1;
59
+ return true;
60
+ };
61
+ }
62
+ /**
63
+ * The same fixed-window counter in the Identity database (table rate_limit_windows), so the limit
64
+ * holds across every replica and survives a restart (ST-4). One statement counts the use and
65
+ * starts a new window when the old one is over. `name` keeps the counters of different limits
66
+ * apart. If the database cannot be reached the use is counted in this process instead.
67
+ */
68
+ function createSharedRateLimiter(prisma, params) {
69
+ const { name, limit, windowMs } = params;
70
+ const local = createRateLimiter({ limit, windowMs });
71
+ const db = prisma;
72
+ if (typeof db.$queryRaw !== "function") {
73
+ // A stand-in client without SQL (unit tests): this process only.
74
+ return (key) => __awaiter(this, void 0, void 0, function* () { return local(`${name}:${key}`); });
75
+ }
76
+ const sql = db;
77
+ let lastPrune = 0;
78
+ return function tryConsume(key) {
79
+ return __awaiter(this, void 0, void 0, function* () {
80
+ var _a, _b;
81
+ const k = `${name}:${key}`.slice(0, 255);
82
+ try {
83
+ const rows = yield sql.$queryRaw `
84
+ INSERT INTO "rate_limit_windows" ("key", "started_at", "count")
85
+ VALUES (${k}, now(), 1)
86
+ ON CONFLICT ("key") DO UPDATE SET
87
+ "started_at" = CASE
88
+ WHEN "rate_limit_windows"."started_at" <= now() - (${windowMs} * interval '1 millisecond')
89
+ THEN now() ELSE "rate_limit_windows"."started_at" END,
90
+ "count" = CASE
91
+ WHEN "rate_limit_windows"."started_at" <= now() - (${windowMs} * interval '1 millisecond')
92
+ THEN 1 ELSE "rate_limit_windows"."count" + 1 END
93
+ RETURNING "count"`;
94
+ if (Date.now() - lastPrune > windowMs) {
95
+ lastPrune = Date.now();
96
+ void sql.$executeRaw `
97
+ DELETE FROM "rate_limit_windows"
98
+ WHERE "started_at" < now() - (${windowMs} * interval '1 millisecond')`.catch(() => undefined);
99
+ }
100
+ return Number((_b = (_a = rows[0]) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 1) <= limit;
101
+ }
102
+ catch (e) {
103
+ logger.warn(`rate limit ${name}: database unavailable (${e.message}); counting in this process`);
104
+ return local(k);
105
+ }
106
+ });
107
+ };
108
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
3
+ * http://github.com/fonoster/fonoster
4
+ *
5
+ * This file is part of Fonoster
6
+ *
7
+ * Licensed under the MIT License (the "License");
8
+ * you may not use this file except in compliance with
9
+ * the License. You may obtain a copy of the License at
10
+ *
11
+ * https://opensource.org/licenses/MIT
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import { status } from "@grpc/grpc-js";
20
+ type SignInFailureLimits = {
21
+ perEmail: number;
22
+ perIp: number;
23
+ windowMs: number;
24
+ };
25
+ /**
26
+ * Slows down password and code guessing on sign-in. Failed attempts are
27
+ * counted per email and per client IP; once either reaches its limit, sign-in
28
+ * for that email (or from that IP) is refused for one window, right or wrong.
29
+ * Only failures count, so someone who knows an email can't lock its owner out
30
+ * by signing in successfully elsewhere, and a user who signs in fine is never
31
+ * slowed down.
32
+ *
33
+ * Kept in this process's memory, like the other Identity limits: with several
34
+ * replicas each enforces its own limit.
35
+ */
36
+ declare function createSignInThrottle(limits?: SignInFailureLimits): {
37
+ /** True while sign-in for this email, or from this IP, is refused. */
38
+ isBlocked(email: string, ip: string, now?: number): boolean;
39
+ /** Counts one failed sign-in for the email and the IP. */
40
+ recordFailure(email: string, ip: string, now?: number): void;
41
+ };
42
+ type SignInThrottle = ReturnType<typeof createSignInThrottle>;
43
+ declare const tooManySignInAttempts: {
44
+ code: status;
45
+ message: string;
46
+ };
47
+ export { createSignInThrottle, SignInFailureLimits, SignInThrottle, tooManySignInAttempts };