@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.
- package/README.md +25 -0
- package/dist/allowList.d.ts +4 -0
- package/dist/allowList.js +7 -1
- package/dist/apikeys/createCreateApiKey.js +16 -0
- package/dist/apikeys/createDeleteApiKey.js +22 -2
- package/dist/apikeys/createListApiKeys.js +14 -0
- package/dist/apikeys/createRegenerateApiKey.js +23 -2
- package/dist/config/index.d.ts +20 -0
- package/dist/config/index.js +36 -0
- package/dist/config/publishedKeys.d.ts +9 -0
- package/dist/config/publishedKeys.js +56 -0
- package/dist/config/schemas.d.ts +392 -0
- package/dist/config/schemas.js +163 -0
- package/dist/config/toIdentityConfig.d.ts +19 -0
- package/dist/config/toIdentityConfig.js +93 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +29 -1
- package/dist/exchanges/createExchangeApiKey.js +4 -1
- package/dist/exchanges/createExchangeCredentials.d.ts +6 -1
- package/dist/exchanges/createExchangeCredentials.js +20 -2
- package/dist/exchanges/createExchangeOauth2Code.js +25 -2
- package/dist/exchanges/createExchangeRefreshToken.d.ts +9 -1
- package/dist/exchanges/createExchangeRefreshToken.js +57 -13
- package/dist/exchanges/createExchangeVerificationCode.d.ts +35 -0
- package/dist/exchanges/createExchangeVerificationCode.js +95 -0
- package/dist/exchanges/createRevokeToken.d.ts +33 -0
- package/dist/exchanges/createRevokeToken.js +64 -0
- package/dist/exchanges/exchangeTokens.d.ts +3 -1
- package/dist/exchanges/exchangeTokens.js +23 -5
- package/dist/exchanges/index.d.ts +2 -0
- package/dist/exchanges/index.js +2 -0
- package/dist/exchanges/payloads/users/createGetIdTokenPayload.js +3 -2
- package/dist/exchanges/refreshTokens.d.ts +30 -0
- package/dist/exchanges/refreshTokens.js +101 -0
- package/dist/exchanges/types.d.ts +53 -2
- package/dist/generated/@prisma/client/edge.js +32 -9
- package/dist/generated/@prisma/client/index-browser.js +24 -1
- package/dist/generated/@prisma/client/index.d.ts +3233 -417
- package/dist/generated/@prisma/client/index.js +34 -11
- package/dist/generated/@prisma/client/{libquery_engine-darwin-arm64.dylib.node → libquery_engine-debian-openssl-3.0.x.so.node} +0 -0
- package/dist/generated/@prisma/client/package.json +1 -1
- package/dist/generated/@prisma/client/schema.prisma +47 -7
- package/dist/generated/@prisma/client/wasm.js +32 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server/config.d.ts +286 -55
- package/dist/server/config.js +74 -108
- package/dist/server/index.js +5 -3
- package/dist/service.d.ts +6 -0
- package/dist/service.js +11 -4
- package/dist/users/createCreateUser.d.ts +6 -1
- package/dist/users/createCreateUser.js +17 -9
- package/dist/users/createCreateUserWithOauth2Code.js +19 -15
- package/dist/users/createResetPassword.d.ts +5 -0
- package/dist/users/createResetPassword.js +33 -7
- package/dist/users/createSendResetPasswordCode.d.ts +6 -0
- package/dist/users/createSendResetPasswordCode.js +33 -1
- package/dist/users/createUpdateUser.d.ts +7 -0
- package/dist/users/createUpdateUser.js +41 -8
- package/dist/users/createUserFromGitHub.d.ts +40 -0
- package/dist/users/createUserFromGitHub.js +27 -0
- package/dist/users/index.d.ts +1 -0
- package/dist/users/index.js +1 -0
- package/dist/users/migratePasswordStorage.d.ts +7 -0
- package/dist/users/migratePasswordStorage.js +47 -0
- package/dist/users/types.d.ts +3 -1
- package/dist/users/upsertDefaultUser.js +6 -14
- package/dist/utils/authMethods.d.ts +31 -0
- package/dist/utils/authMethods.js +44 -0
- package/dist/utils/claimUnverifiedAccount.d.ts +35 -0
- package/dist/utils/claimUnverifiedAccount.js +43 -0
- package/dist/utils/clientAddress.d.ts +26 -0
- package/dist/utils/clientAddress.js +9 -0
- package/dist/utils/createGenerateVerificationCode.d.ts +4 -0
- package/dist/utils/createGenerateVerificationCode.js +12 -2
- package/dist/utils/createGenerateWorkspaceInviteToken.js +3 -0
- package/dist/utils/createGetApiKeyByAccessKeyId.d.ts +5 -1
- package/dist/utils/createGetApiKeyByAccessKeyId.js +5 -0
- package/dist/utils/createGetUserByAccessKeyId.d.ts +3 -1
- package/dist/utils/createGetUserByEmail.d.ts +3 -1
- package/dist/utils/createGetUserByGitHubEmails.d.ts +41 -0
- package/dist/utils/createGetUserByGitHubEmails.js +31 -0
- package/dist/utils/createGetUserByWorkspaceAccessKeyId.d.ts +3 -1
- package/dist/utils/createIsValidVerificationCode.d.ts +5 -0
- package/dist/utils/createIsValidVerificationCode.js +34 -7
- package/dist/utils/createSendEmail.d.ts +2 -1
- package/dist/utils/createSendEmail.js +17 -0
- package/dist/utils/createUpdateMembershipStatus.js +10 -3
- package/dist/utils/generateOneTimePassword.d.ts +3 -0
- package/dist/utils/generateOneTimePassword.js +25 -0
- package/dist/utils/getGitHubUserWithOauth2Code.d.ts +20 -2
- package/dist/utils/getGitHubUserWithOauth2Code.js +41 -9
- package/dist/utils/isWorkspaceAdminCall.d.ts +8 -0
- package/dist/utils/isWorkspaceAdminCall.js +47 -0
- package/dist/utils/normalizeContact.d.ts +22 -0
- package/dist/utils/normalizeContact.js +28 -0
- package/dist/utils/passwords.d.ts +11 -0
- package/dist/utils/passwords.js +84 -0
- package/dist/utils/rateLimiter.d.ts +20 -0
- package/dist/utils/rateLimiter.js +108 -0
- package/dist/utils/signInThrottle.d.ts +47 -0
- package/dist/utils/signInThrottle.js +90 -0
- package/dist/utils/userPasswords.d.ts +25 -0
- package/dist/utils/userPasswords.js +98 -0
- package/dist/verification/createSendVerificationCode.js +27 -1
- package/dist/verification/createVerifyCode.js +10 -0
- package/dist/workspaces/createCreateWorkspace.d.ts +7 -1
- package/dist/workspaces/createCreateWorkspace.js +41 -5
- package/dist/workspaces/createDeleteWorkspace.d.ts +2 -1
- package/dist/workspaces/createDeleteWorkspace.js +16 -2
- package/dist/workspaces/createInviteUserToWorkspace.js +19 -17
- package/dist/workspaces/createResendWorkspaceMembershipInvitation.js +17 -2
- package/dist/workspaces/createUpdateWorkspace.js +18 -4
- package/dist/workspaces/createWorkspaceDirectory.d.ts +29 -0
- package/dist/workspaces/createWorkspaceDirectory.js +42 -0
- package/dist/workspaces/index.d.ts +1 -0
- package/dist/workspaces/index.js +1 -0
- package/package.json +7 -6
- 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,
|
|
35
|
-
|
|
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(
|
|
48
|
+
lt: new Date(now.getTime() - constants_1.VERIFICATION_CODE_EXPIRATION)
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
});
|
|
43
|
-
const
|
|
52
|
+
const current = yield prisma.verificationCode.findFirst({
|
|
44
53
|
where: {
|
|
45
54
|
type,
|
|
46
55
|
value,
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
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 } =
|
|
49
|
+
const { memberRef } = claims;
|
|
43
50
|
yield prisma.workspaceMember.update({
|
|
44
51
|
where: {
|
|
45
52
|
ref: memberRef
|
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
|
32
|
-
const
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 };
|