@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
@@ -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, common_1.createSendEmail)(identityConfig.smtpConfig);
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
- declare function createCreateWorkspace(prisma: Prisma): (call: {
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
- function createCreateWorkspace(prisma) {
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", { name, ownerRef });
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
- const { ref } = workspace;
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
- declare function createDeleteWorkspace(prisma: Prisma): (call: {
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, password } = request;
67
+ const { name, email } = request;
61
68
  return yield prisma.user.create({
62
- data: {
63
- name,
64
- email,
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, nanoid_1.customAlphabet)("1234567890abcdef", 10)();
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
- isExistingUser,
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: member.user.password,
103
+ oneTimePassword,
89
104
  workspaceName: member.workspace.name,
90
- isExistingUser: true,
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 createIsWorkspaceMember_1 = require("./createIsWorkspaceMember");
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
- const isMember = yield (0, createIsWorkspaceMember_1.createIsWorkspaceMember)(prisma)(ref, userRef);
45
- if (!isMember) {
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: "User is not a member of the workspace"
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
+ }
@@ -25,3 +25,4 @@ export * from "./createListWorkspaceMembers";
25
25
  export * from "./createRemoveUserFromWorkspace";
26
26
  export * from "./createResendWorkspaceMembershipInvitation";
27
27
  export * from "./createUpdateWorkspace";
28
+ export * from "./createWorkspaceDirectory";
@@ -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);