@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tooManySignInAttempts = void 0;
|
|
4
|
+
exports.createSignInThrottle = createSignInThrottle;
|
|
5
|
+
/**
|
|
6
|
+
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
7
|
+
* http://github.com/fonoster/fonoster
|
|
8
|
+
*
|
|
9
|
+
* This file is part of Fonoster
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the MIT License (the "License");
|
|
12
|
+
* you may not use this file except in compliance with
|
|
13
|
+
* the License. You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* https://opensource.org/licenses/MIT
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License.
|
|
22
|
+
*/
|
|
23
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
24
|
+
const constants_1 = require("../constants");
|
|
25
|
+
const rateLimiter_1 = require("./rateLimiter");
|
|
26
|
+
/**
|
|
27
|
+
* Slows down password and code guessing on sign-in. Failed attempts are
|
|
28
|
+
* counted per email and per client IP; once either reaches its limit, sign-in
|
|
29
|
+
* for that email (or from that IP) is refused for one window, right or wrong.
|
|
30
|
+
* Only failures count, so someone who knows an email can't lock its owner out
|
|
31
|
+
* by signing in successfully elsewhere, and a user who signs in fine is never
|
|
32
|
+
* slowed down.
|
|
33
|
+
*
|
|
34
|
+
* Kept in this process's memory, like the other Identity limits: with several
|
|
35
|
+
* replicas each enforces its own limit.
|
|
36
|
+
*/
|
|
37
|
+
function createSignInThrottle(limits = constants_1.SIGN_IN_FAILURE_LIMITS) {
|
|
38
|
+
const { perEmail, perIp, windowMs } = limits;
|
|
39
|
+
// tryConsume refuses the use after `limit`, so `limit - 1` makes the
|
|
40
|
+
// limit-th failure the one that blocks.
|
|
41
|
+
const emailFailures = (0, rateLimiter_1.createRateLimiter)({
|
|
42
|
+
limit: Math.max(perEmail - 1, 0),
|
|
43
|
+
windowMs
|
|
44
|
+
});
|
|
45
|
+
const ipFailures = (0, rateLimiter_1.createRateLimiter)({
|
|
46
|
+
limit: Math.max(perIp - 1, 0),
|
|
47
|
+
windowMs
|
|
48
|
+
});
|
|
49
|
+
const blockedUntil = new Map();
|
|
50
|
+
const emailKey = (email) => `email:${email.trim().toLowerCase()}`;
|
|
51
|
+
const ipKey = (ip) => `ip:${ip}`;
|
|
52
|
+
const isBlocked = (key, now) => {
|
|
53
|
+
const until = blockedUntil.get(key);
|
|
54
|
+
if (until === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
if (now >= until) {
|
|
57
|
+
blockedUntil.delete(key);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
const block = (key, now) => {
|
|
63
|
+
blockedUntil.set(key, now + windowMs);
|
|
64
|
+
// Keep the map from growing without bound.
|
|
65
|
+
if (blockedUntil.size > 10000) {
|
|
66
|
+
blockedUntil.forEach((until, k) => {
|
|
67
|
+
if (now >= until)
|
|
68
|
+
blockedUntil.delete(k);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
/** True while sign-in for this email, or from this IP, is refused. */
|
|
74
|
+
isBlocked(email, ip, now = Date.now()) {
|
|
75
|
+
return isBlocked(ipKey(ip), now) || isBlocked(emailKey(email), now);
|
|
76
|
+
},
|
|
77
|
+
/** Counts one failed sign-in for the email and the IP. */
|
|
78
|
+
recordFailure(email, ip, now = Date.now()) {
|
|
79
|
+
if (!ipFailures(ipKey(ip), now))
|
|
80
|
+
block(ipKey(ip), now);
|
|
81
|
+
if (!emailFailures(emailKey(email), now))
|
|
82
|
+
block(emailKey(email), now);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const tooManySignInAttempts = {
|
|
87
|
+
code: grpc_js_1.status.RESOURCE_EXHAUSTED,
|
|
88
|
+
message: "Too many sign-in attempts; try again later"
|
|
89
|
+
};
|
|
90
|
+
exports.tooManySignInAttempts = tooManySignInAttempts;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Prisma } from "../db";
|
|
2
|
+
type UserWithPassword = {
|
|
3
|
+
ref: string;
|
|
4
|
+
password?: string | null;
|
|
5
|
+
passwordDigest?: string | null;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Columns to write when setting a password: the digest only, clearing the
|
|
9
|
+
* legacy encrypted value.
|
|
10
|
+
*/
|
|
11
|
+
declare function passwordColumns(password: string): Promise<{
|
|
12
|
+
passwordDigest: string;
|
|
13
|
+
password: any;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Verifies a user's password. Users from before digests existed are checked
|
|
17
|
+
* against their decrypted legacy password, then migrated to a digest.
|
|
18
|
+
*/
|
|
19
|
+
declare function createVerifyUserPassword(prisma: Prisma): (user: UserWithPassword, password: string) => Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Moves every remaining legacy password to a digest. Batched and idempotent:
|
|
22
|
+
* only users with a legacy value and no digest are touched.
|
|
23
|
+
*/
|
|
24
|
+
declare function backfillPasswordDigests(prisma: Prisma): Promise<number>;
|
|
25
|
+
export { backfillPasswordDigests, createVerifyUserPassword, passwordColumns };
|
|
@@ -0,0 +1,98 @@
|
|
|
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.backfillPasswordDigests = backfillPasswordDigests;
|
|
13
|
+
exports.createVerifyUserPassword = createVerifyUserPassword;
|
|
14
|
+
exports.passwordColumns = passwordColumns;
|
|
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 logger_1 = require("@fonoster/logger");
|
|
34
|
+
const passwords_1 = require("./passwords");
|
|
35
|
+
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
36
|
+
const BACKFILL_BATCH_SIZE = 500;
|
|
37
|
+
/**
|
|
38
|
+
* Columns to write when setting a password: the digest only, clearing the
|
|
39
|
+
* legacy encrypted value.
|
|
40
|
+
*/
|
|
41
|
+
function passwordColumns(password) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
return { passwordDigest: yield (0, passwords_1.hashPassword)(password), password: null };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verifies a user's password. Users from before digests existed are checked
|
|
48
|
+
* against their decrypted legacy password, then migrated to a digest.
|
|
49
|
+
*/
|
|
50
|
+
function createVerifyUserPassword(prisma) {
|
|
51
|
+
return (user, password) => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
if (!password) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (user.passwordDigest) {
|
|
56
|
+
return (0, passwords_1.verifyPasswordDigest)(password, user.passwordDigest);
|
|
57
|
+
}
|
|
58
|
+
if (!user.password || !(0, passwords_1.equalsInConstantTime)(user.password, password)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
yield prisma.user.update({
|
|
62
|
+
where: { ref: user.ref },
|
|
63
|
+
data: yield passwordColumns(password)
|
|
64
|
+
});
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Moves every remaining legacy password to a digest. Batched and idempotent:
|
|
70
|
+
* only users with a legacy value and no digest are touched.
|
|
71
|
+
*/
|
|
72
|
+
function backfillPasswordDigests(prisma) {
|
|
73
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
74
|
+
const migrateBatch = () => __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
const users = yield prisma.user.findMany({
|
|
76
|
+
where: { passwordDigest: null, password: { not: null } },
|
|
77
|
+
select: { ref: true, password: true },
|
|
78
|
+
take: BACKFILL_BATCH_SIZE
|
|
79
|
+
});
|
|
80
|
+
if (users.length === 0) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
// One at a time: each scrypt derivation holds ~32 MiB.
|
|
84
|
+
yield users.reduce((previous, user) => previous.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
85
|
+
yield prisma.user.update({
|
|
86
|
+
where: { ref: user.ref },
|
|
87
|
+
data: yield passwordColumns(user.password)
|
|
88
|
+
});
|
|
89
|
+
})), Promise.resolve());
|
|
90
|
+
return users.length + (yield migrateBatch());
|
|
91
|
+
});
|
|
92
|
+
const migrated = yield migrateBatch();
|
|
93
|
+
if (migrated > 0) {
|
|
94
|
+
logger.info("moved legacy passwords to digests", { users: migrated });
|
|
95
|
+
}
|
|
96
|
+
return migrated;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -30,19 +30,45 @@ exports.createSendVerificationCode = createSendVerificationCode;
|
|
|
30
30
|
*/
|
|
31
31
|
const common_1 = require("@fonoster/common");
|
|
32
32
|
const logger_1 = require("@fonoster/logger");
|
|
33
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
34
|
+
const constants_1 = require("../constants");
|
|
35
|
+
const clientAddress_1 = require("../utils/clientAddress");
|
|
33
36
|
const createGenerateVerificationCode_1 = require("../utils/createGenerateVerificationCode");
|
|
37
|
+
const createSendEmail_1 = require("../utils/createSendEmail");
|
|
38
|
+
const rateLimiter_1 = require("../utils/rateLimiter");
|
|
34
39
|
const sendVerificationEmail_1 = require("./sendVerificationEmail");
|
|
35
40
|
const sendVerificationMessage_1 = require("./sendVerificationMessage");
|
|
36
41
|
const types_1 = require("./types");
|
|
37
42
|
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
43
|
+
const tooManyRequests = {
|
|
44
|
+
code: grpc_js_1.status.RESOURCE_EXHAUSTED,
|
|
45
|
+
message: "Too many verification codes requested; try again later"
|
|
46
|
+
};
|
|
38
47
|
function createSendVerificationCode(prisma, identityConfig) {
|
|
39
48
|
const sendSms = (0, common_1.createSendSmsTwilioImpl)(identityConfig.twilioSmsConfig);
|
|
40
|
-
const sendEmail = (0,
|
|
49
|
+
const sendEmail = (0, createSendEmail_1.createSendEmail)(identityConfig);
|
|
41
50
|
const generateVerificationCode = (0, createGenerateVerificationCode_1.createGenerateVerificationCode)(prisma);
|
|
51
|
+
const { perEmail, perIp, windowMs } = constants_1.VERIFICATION_CODE_SEND_LIMITS;
|
|
52
|
+
const withinContactLimit = (0, rateLimiter_1.createSharedRateLimiter)(prisma, {
|
|
53
|
+
name: "verification:contact",
|
|
54
|
+
limit: perEmail,
|
|
55
|
+
windowMs
|
|
56
|
+
});
|
|
57
|
+
const withinIpLimit = (0, rateLimiter_1.createSharedRateLimiter)(prisma, {
|
|
58
|
+
name: "verification:ip",
|
|
59
|
+
limit: perIp,
|
|
60
|
+
windowMs
|
|
61
|
+
});
|
|
62
|
+
// The response never says whether the contact belongs to an account.
|
|
42
63
|
const fn = (call, callback) => __awaiter(this, void 0, void 0, function* () {
|
|
43
64
|
var _a;
|
|
44
65
|
const { request } = call;
|
|
45
66
|
const actualContactType = (_a = request.contactType) !== null && _a !== void 0 ? _a : types_1.ContactType.EMAIL;
|
|
67
|
+
const contact = `${actualContactType}:${request.value.toLowerCase()}`;
|
|
68
|
+
if (!(yield withinIpLimit((0, clientAddress_1.clientAddress)(call))) ||
|
|
69
|
+
!(yield withinContactLimit(contact))) {
|
|
70
|
+
return callback(tooManyRequests);
|
|
71
|
+
}
|
|
46
72
|
const verificationCode = yield generateVerificationCode({
|
|
47
73
|
type: actualContactType,
|
|
48
74
|
value: request.value
|
|
@@ -38,6 +38,16 @@ function createVerifyCode(prisma) {
|
|
|
38
38
|
const { request } = call;
|
|
39
39
|
const { username, contactType, value, verificationCode } = request;
|
|
40
40
|
const actualContactType = contactType !== null && contactType !== void 0 ? contactType : types_1.ContactType.EMAIL;
|
|
41
|
+
const sameEmail = (a, b) => (a !== null && a !== void 0 ? a : "").trim().toLowerCase() === (b !== null && b !== void 0 ? b : "").trim().toLowerCase();
|
|
42
|
+
// A code proves only the address it was sent to: an email code verifies
|
|
43
|
+
// the account with that email, never another one (M2).
|
|
44
|
+
if (actualContactType === types_1.ContactType.EMAIL &&
|
|
45
|
+
!sameEmail(value, username)) {
|
|
46
|
+
return callback({
|
|
47
|
+
code: grpc_js_1.status.PERMISSION_DENIED,
|
|
48
|
+
message: "Invalid verification code"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
41
51
|
const isValid = yield isValidVerificationCode({
|
|
42
52
|
type: actualContactType,
|
|
43
53
|
value,
|
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { GrpcErrorMessage } from "@fonoster/common";
|
|
20
20
|
import { Prisma } from "../db";
|
|
21
|
-
|
|
21
|
+
import { WorkspaceRegionHooks } from "../exchanges/types";
|
|
22
|
+
/**
|
|
23
|
+
* Creates a workspace in one region, for its whole life (add-private-regions PR-1, PR-15): an
|
|
24
|
+
* offered public region id (default `default`) or one of the caller's private regions (by id or
|
|
25
|
+
* ref). Private regions are created at the user level, never here.
|
|
26
|
+
*/
|
|
27
|
+
declare function createCreateWorkspace(prisma: Prisma, regions?: WorkspaceRegionHooks): (call: {
|
|
22
28
|
request: unknown;
|
|
23
29
|
}, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
|
|
24
30
|
export { createCreateWorkspace };
|
|
@@ -30,26 +30,62 @@ exports.createCreateWorkspace = createCreateWorkspace;
|
|
|
30
30
|
*/
|
|
31
31
|
const common_1 = require("@fonoster/common");
|
|
32
32
|
const logger_1 = require("@fonoster/logger");
|
|
33
|
+
const grpc_js_1 = require("@grpc/grpc-js");
|
|
33
34
|
const generateAccessKeyId_1 = require("../utils/generateAccessKeyId");
|
|
34
35
|
const getUserRefFromToken_1 = require("../utils/getUserRefFromToken");
|
|
35
36
|
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
36
|
-
|
|
37
|
+
const DEFAULT_REGION = "default";
|
|
38
|
+
/**
|
|
39
|
+
* Creates a workspace in one region, for its whole life (add-private-regions PR-1, PR-15): an
|
|
40
|
+
* offered public region id (default `default`) or one of the caller's private regions (by id or
|
|
41
|
+
* ref). Private regions are created at the user level, never here.
|
|
42
|
+
*/
|
|
43
|
+
function createCreateWorkspace(prisma, regions) {
|
|
37
44
|
const createWorkspace = (call, callback) => __awaiter(this, void 0, void 0, function* () {
|
|
38
45
|
const { request } = call;
|
|
39
46
|
const { name } = request;
|
|
47
|
+
const requested = request.region || undefined;
|
|
40
48
|
const token = (0, common_1.getTokenFromCall)(call);
|
|
41
49
|
const ownerRef = (0, getUserRefFromToken_1.getUserRefFromToken)(token);
|
|
42
50
|
const accessKeyId = (0, generateAccessKeyId_1.generateAccessKeyId)(generateAccessKeyId_1.AccessKeyIdType.WORKSPACE);
|
|
43
|
-
logger.verbose("call to createWorkspace", {
|
|
51
|
+
logger.verbose("call to createWorkspace", {
|
|
52
|
+
name,
|
|
53
|
+
ownerRef,
|
|
54
|
+
region: requested
|
|
55
|
+
});
|
|
56
|
+
let region;
|
|
57
|
+
if (regions) {
|
|
58
|
+
try {
|
|
59
|
+
region = (yield regions.resolveRegion(ownerRef, requested)).region;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
callback({
|
|
63
|
+
code: grpc_js_1.status.INVALID_ARGUMENT,
|
|
64
|
+
message: e.message
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
region = requested !== null && requested !== void 0 ? requested : DEFAULT_REGION;
|
|
71
|
+
if (region !== DEFAULT_REGION) {
|
|
72
|
+
callback({
|
|
73
|
+
code: grpc_js_1.status.INVALID_ARGUMENT,
|
|
74
|
+
message: `region must be ${DEFAULT_REGION}`
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
44
79
|
const workspace = yield prisma.workspace.create({
|
|
45
80
|
data: {
|
|
46
81
|
name,
|
|
47
82
|
accessKeyId,
|
|
48
|
-
ownerRef
|
|
83
|
+
ownerRef,
|
|
84
|
+
region
|
|
49
85
|
}
|
|
50
86
|
});
|
|
51
|
-
|
|
52
|
-
callback(null, { ref });
|
|
87
|
+
regions === null || regions === void 0 ? void 0 : regions.workspaceChanged({ accessKeyId, region });
|
|
88
|
+
callback(null, { ref: workspace.ref, region });
|
|
53
89
|
});
|
|
54
90
|
return (0, common_1.withErrorHandlingAndValidation)(createWorkspace, common_1.Validators.createWorkspaceRequestSchema);
|
|
55
91
|
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { GrpcErrorMessage } from "@fonoster/common";
|
|
20
20
|
import { Prisma } from "../db";
|
|
21
|
-
|
|
21
|
+
import { WorkspaceRegionHooks } from "../exchanges/types";
|
|
22
|
+
declare function createDeleteWorkspace(prisma: Prisma, regions?: WorkspaceRegionHooks): (call: {
|
|
22
23
|
request: unknown;
|
|
23
24
|
}, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
|
|
24
25
|
export { createDeleteWorkspace };
|
|
@@ -32,19 +32,33 @@ const common_1 = require("@fonoster/common");
|
|
|
32
32
|
const logger_1 = require("@fonoster/logger");
|
|
33
33
|
const getUserRefFromToken_1 = require("../utils/getUserRefFromToken");
|
|
34
34
|
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
35
|
-
function createDeleteWorkspace(prisma) {
|
|
35
|
+
function createDeleteWorkspace(prisma, regions) {
|
|
36
36
|
const deleteWorkspace = (call, callback) => __awaiter(this, void 0, void 0, function* () {
|
|
37
37
|
const { request } = call;
|
|
38
38
|
const { ref } = request;
|
|
39
39
|
const token = (0, common_1.getTokenFromCall)(call);
|
|
40
40
|
const ownerRef = (0, getUserRefFromToken_1.getUserRefFromToken)(token);
|
|
41
41
|
logger.verbose("deleting workspace from the system", { ref, ownerRef });
|
|
42
|
-
yield prisma.workspace.delete({
|
|
42
|
+
const deleted = yield prisma.workspace.delete({
|
|
43
43
|
where: {
|
|
44
44
|
ref,
|
|
45
45
|
ownerRef
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
+
// The region stays (it belongs to the user, PR-14); it stops serving this workspace.
|
|
49
|
+
if (deleted === null || deleted === void 0 ? void 0 : deleted.region)
|
|
50
|
+
try {
|
|
51
|
+
regions === null || regions === void 0 ? void 0 : regions.workspaceChanged({
|
|
52
|
+
accessKeyId: deleted.accessKeyId,
|
|
53
|
+
region: deleted.region
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
logger.error("could not resync the workspace's region", {
|
|
58
|
+
ref,
|
|
59
|
+
error: e.message
|
|
60
|
+
});
|
|
61
|
+
}
|
|
48
62
|
callback(null, { ref });
|
|
49
63
|
});
|
|
50
64
|
return (0, common_1.withErrorHandlingAndValidation)(deleteWorkspace, common_1.Validators.emptySchema);
|
|
@@ -32,10 +32,12 @@ const common_1 = require("@fonoster/common");
|
|
|
32
32
|
const logger_1 = require("@fonoster/logger");
|
|
33
33
|
const types_1 = require("@fonoster/types");
|
|
34
34
|
const grpc_js_1 = require("@grpc/grpc-js");
|
|
35
|
-
const nanoid_1 = require("nanoid");
|
|
36
35
|
const utils_1 = require("../utils");
|
|
36
|
+
const authMethods_1 = require("../utils/authMethods");
|
|
37
37
|
const createGenerateWorkspaceInviteToken_1 = require("../utils/createGenerateWorkspaceInviteToken");
|
|
38
|
+
const generateOneTimePassword_1 = require("../utils/generateOneTimePassword");
|
|
38
39
|
const getUserRefFromToken_1 = require("../utils/getUserRefFromToken");
|
|
40
|
+
const userPasswords_1 = require("../utils/userPasswords");
|
|
39
41
|
const createIsAdminMember_1 = require("./createIsAdminMember");
|
|
40
42
|
const createIsWorkspaceMember_1 = require("./createIsWorkspaceMember");
|
|
41
43
|
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
@@ -54,17 +56,19 @@ const findUserByEmail = (prisma, email) => __awaiter(void 0, void 0, void 0, fun
|
|
|
54
56
|
}
|
|
55
57
|
});
|
|
56
58
|
});
|
|
59
|
+
/**
|
|
60
|
+
* Creates the invitee. With password sign-in they get a one-time password
|
|
61
|
+
* they must replace after signing in; otherwise they sign in by email code or
|
|
62
|
+
* GitHub and have no password.
|
|
63
|
+
*/
|
|
57
64
|
const createCreateUser = (prisma) => {
|
|
58
|
-
return function createUser(request) {
|
|
65
|
+
return function createUser(request, oneTimePassword) {
|
|
59
66
|
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
-
const { name, email
|
|
67
|
+
const { name, email } = request;
|
|
61
68
|
return yield prisma.user.create({
|
|
62
|
-
data: {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
accessKeyId: (0, utils_1.generateAccessKeyId)(utils_1.AccessKeyIdType.USER),
|
|
66
|
-
password
|
|
67
|
-
}
|
|
69
|
+
data: Object.assign({ name,
|
|
70
|
+
email, accessKeyId: (0, utils_1.generateAccessKeyId)(utils_1.AccessKeyIdType.USER) }, (oneTimePassword
|
|
71
|
+
? Object.assign(Object.assign({}, (yield (0, userPasswords_1.passwordColumns)(oneTimePassword))), { mustChangePassword: true }) : {}))
|
|
68
72
|
});
|
|
69
73
|
});
|
|
70
74
|
};
|
|
@@ -96,16 +100,13 @@ function createInviteUserToWorkspace(prisma, identityConfig, sendInvite) {
|
|
|
96
100
|
if (isMember) {
|
|
97
101
|
return callback(userIsMemberError);
|
|
98
102
|
}
|
|
99
|
-
const oneTimePassword = (0,
|
|
103
|
+
const oneTimePassword = (0, authMethods_1.isMethodEnabled)(identityConfig, "password")
|
|
104
|
+
? (0, generateOneTimePassword_1.generateOneTimePassword)()
|
|
105
|
+
: undefined;
|
|
100
106
|
let isExistingUser = true;
|
|
101
107
|
if (!user) {
|
|
102
108
|
isExistingUser = false;
|
|
103
|
-
user = yield createCreateUser(prisma)({
|
|
104
|
-
name,
|
|
105
|
-
email,
|
|
106
|
-
password: oneTimePassword,
|
|
107
|
-
role
|
|
108
|
-
});
|
|
109
|
+
user = yield createCreateUser(prisma)({ name, email }, oneTimePassword);
|
|
109
110
|
}
|
|
110
111
|
const newMember = yield prisma.workspaceMember.create({
|
|
111
112
|
data: {
|
|
@@ -128,7 +129,8 @@ function createInviteUserToWorkspace(prisma, identityConfig, sendInvite) {
|
|
|
128
129
|
recipient: email,
|
|
129
130
|
oneTimePassword,
|
|
130
131
|
workspaceName: newMember.workspace.name,
|
|
131
|
-
|
|
132
|
+
// Without a one-time password the "existing user" email (a link) fits.
|
|
133
|
+
isExistingUser: isExistingUser || !oneTimePassword,
|
|
132
134
|
inviteUrl: `${identityConfig.workspaceInviteUrl}?token=${inviteeToken}`
|
|
133
135
|
});
|
|
134
136
|
callback(null, {
|
|
@@ -32,8 +32,11 @@ const common_1 = require("@fonoster/common");
|
|
|
32
32
|
const logger_1 = require("@fonoster/logger");
|
|
33
33
|
const grpc_js_1 = require("@grpc/grpc-js");
|
|
34
34
|
const utils_1 = require("../utils");
|
|
35
|
+
const authMethods_1 = require("../utils/authMethods");
|
|
35
36
|
const createGenerateWorkspaceInviteToken_1 = require("../utils/createGenerateWorkspaceInviteToken");
|
|
37
|
+
const generateOneTimePassword_1 = require("../utils/generateOneTimePassword");
|
|
36
38
|
const getUserRefFromToken_1 = require("../utils/getUserRefFromToken");
|
|
39
|
+
const userPasswords_1 = require("../utils/userPasswords");
|
|
37
40
|
const createIsAdminMember_1 = require("./createIsAdminMember");
|
|
38
41
|
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
39
42
|
function createResendWorkspaceMembershipInvitation(prisma, identityConfig, sendInvite) {
|
|
@@ -83,11 +86,23 @@ function createResendWorkspaceMembershipInvitation(prisma, identityConfig, sendI
|
|
|
83
86
|
accessKeyId: member.user.accessKeyId,
|
|
84
87
|
expiresIn: identityConfig.workspaceInviteExpiration
|
|
85
88
|
});
|
|
89
|
+
// Stored passwords can't be read back (they are digests). An invitee who
|
|
90
|
+
// never replaced their one-time password gets a new one.
|
|
91
|
+
const oneTimePassword = member.user.mustChangePassword &&
|
|
92
|
+
(0, authMethods_1.isMethodEnabled)(identityConfig, "password")
|
|
93
|
+
? (0, generateOneTimePassword_1.generateOneTimePassword)()
|
|
94
|
+
: undefined;
|
|
95
|
+
if (oneTimePassword) {
|
|
96
|
+
yield prisma.user.update({
|
|
97
|
+
where: { ref: member.user.ref },
|
|
98
|
+
data: yield (0, userPasswords_1.passwordColumns)(oneTimePassword)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
86
101
|
yield sendInvite((0, utils_1.createSendEmail)(identityConfig), {
|
|
87
102
|
recipient: member.user.email,
|
|
88
|
-
oneTimePassword
|
|
103
|
+
oneTimePassword,
|
|
89
104
|
workspaceName: member.workspace.name,
|
|
90
|
-
isExistingUser:
|
|
105
|
+
isExistingUser: !oneTimePassword,
|
|
91
106
|
inviteUrl: `${identityConfig.workspaceInviteUrl}?token=${inviteeToken}`
|
|
92
107
|
});
|
|
93
108
|
callback(null, {
|
|
@@ -32,7 +32,7 @@ const common_1 = require("@fonoster/common");
|
|
|
32
32
|
const logger_1 = require("@fonoster/logger");
|
|
33
33
|
const grpc_js_1 = require("@grpc/grpc-js");
|
|
34
34
|
const getUserRefFromToken_1 = require("../utils/getUserRefFromToken");
|
|
35
|
-
const
|
|
35
|
+
const createIsAdminMember_1 = require("./createIsAdminMember");
|
|
36
36
|
const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
|
|
37
37
|
function createUpdateWorkspace(prisma) {
|
|
38
38
|
const updateWorkspace = (call, callback) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -40,13 +40,27 @@ function createUpdateWorkspace(prisma) {
|
|
|
40
40
|
const userRef = (0, getUserRefFromToken_1.getUserRefFromToken)(token);
|
|
41
41
|
const { request } = call;
|
|
42
42
|
const { ref, name } = request;
|
|
43
|
+
const region = request.region;
|
|
43
44
|
logger.verbose("call to updateWorkspace", { ref, userRef });
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
// The owner and admins rename a workspace; members only use it.
|
|
46
|
+
const isAdmin = yield (0, createIsAdminMember_1.createIsAdminMember)(prisma)(ref, userRef);
|
|
47
|
+
if (!isAdmin) {
|
|
46
48
|
callback({
|
|
47
49
|
code: grpc_js_1.status.PERMISSION_DENIED,
|
|
48
|
-
message: "
|
|
50
|
+
message: "Only the workspace owner or an admin can update it"
|
|
49
51
|
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// The region is fixed at creation (add-private-regions PR-1).
|
|
55
|
+
if (region) {
|
|
56
|
+
const current = yield prisma.workspace.findUnique({ where: { ref } });
|
|
57
|
+
if (current && current.region !== region) {
|
|
58
|
+
callback({
|
|
59
|
+
code: grpc_js_1.status.FAILED_PRECONDITION,
|
|
60
|
+
message: "region_immutable: a workspace's region cannot change"
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
50
64
|
}
|
|
51
65
|
yield prisma.workspace.update({
|
|
52
66
|
where: {
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { Prisma } from "../db";
|
|
20
|
+
import { RegionWorkspace } from "../exchanges/types";
|
|
21
|
+
/**
|
|
22
|
+
* Read access to workspaces for the control plane's regions code (add-private-regions PR-14):
|
|
23
|
+
* which workspaces a region serves and who owns a workspace. Identity has its own database.
|
|
24
|
+
*/
|
|
25
|
+
declare function createWorkspaceDirectory(prisma: Prisma): {
|
|
26
|
+
workspace(accessKeyId: string): Promise<RegionWorkspace>;
|
|
27
|
+
inRegion(region: string): Promise<RegionWorkspace[]>;
|
|
28
|
+
};
|
|
29
|
+
export { createWorkspaceDirectory };
|
|
@@ -0,0 +1,42 @@
|
|
|
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.createWorkspaceDirectory = createWorkspaceDirectory;
|
|
13
|
+
const pick = (w) => ({
|
|
14
|
+
ref: w.ref,
|
|
15
|
+
accessKeyId: w.accessKeyId,
|
|
16
|
+
name: w.name,
|
|
17
|
+
ownerRef: w.ownerRef,
|
|
18
|
+
region: w.region
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Read access to workspaces for the control plane's regions code (add-private-regions PR-14):
|
|
22
|
+
* which workspaces a region serves and who owns a workspace. Identity has its own database.
|
|
23
|
+
*/
|
|
24
|
+
function createWorkspaceDirectory(prisma) {
|
|
25
|
+
return {
|
|
26
|
+
workspace(accessKeyId) {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const w = yield prisma.workspace.findUnique({ where: { accessKeyId } });
|
|
29
|
+
return w ? pick(w) : undefined;
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
inRegion(region) {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
const rows = yield prisma.workspace.findMany({
|
|
35
|
+
where: { region },
|
|
36
|
+
orderBy: { createdAt: "asc" }
|
|
37
|
+
});
|
|
38
|
+
return rows.map(pick);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/workspaces/index.js
CHANGED
|
@@ -41,3 +41,4 @@ __exportStar(require("./createListWorkspaceMembers"), exports);
|
|
|
41
41
|
__exportStar(require("./createRemoveUserFromWorkspace"), exports);
|
|
42
42
|
__exportStar(require("./createResendWorkspaceMembershipInvitation"), exports);
|
|
43
43
|
__exportStar(require("./createUpdateWorkspace"), exports);
|
|
44
|
+
__exportStar(require("./createWorkspaceDirectory"), exports);
|