@fonoster/identity 0.27.0 → 0.31.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 (127) 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 +27 -4
  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 +31 -8
  40. package/dist/generated/@prisma/client/package.json +1 -1
  41. package/dist/generated/@prisma/client/runtime/edge-esm.js +2 -2
  42. package/dist/generated/@prisma/client/runtime/edge.js +2 -2
  43. package/dist/generated/@prisma/client/runtime/library.js +2 -2
  44. package/dist/generated/@prisma/client/runtime/react-native.js +2 -2
  45. package/dist/generated/@prisma/client/runtime/wasm-compiler-edge.js +2 -2
  46. package/dist/generated/@prisma/client/runtime/wasm-engine-edge.js +2 -2
  47. package/dist/generated/@prisma/client/schema.prisma +47 -7
  48. package/dist/generated/@prisma/client/wasm.js +27 -4
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/server/config.d.ts +286 -55
  52. package/dist/server/config.js +73 -108
  53. package/dist/server/index.js +5 -3
  54. package/dist/service.d.ts +6 -0
  55. package/dist/service.js +11 -4
  56. package/dist/users/createCreateUser.d.ts +6 -1
  57. package/dist/users/createCreateUser.js +17 -9
  58. package/dist/users/createCreateUserWithOauth2Code.js +19 -15
  59. package/dist/users/createResetPassword.d.ts +5 -0
  60. package/dist/users/createResetPassword.js +33 -7
  61. package/dist/users/createSendResetPasswordCode.d.ts +6 -0
  62. package/dist/users/createSendResetPasswordCode.js +33 -1
  63. package/dist/users/createUpdateUser.d.ts +7 -0
  64. package/dist/users/createUpdateUser.js +41 -8
  65. package/dist/users/createUserFromGitHub.d.ts +40 -0
  66. package/dist/users/createUserFromGitHub.js +27 -0
  67. package/dist/users/index.d.ts +1 -0
  68. package/dist/users/index.js +1 -0
  69. package/dist/users/migratePasswordStorage.d.ts +7 -0
  70. package/dist/users/migratePasswordStorage.js +47 -0
  71. package/dist/users/types.d.ts +3 -1
  72. package/dist/users/upsertDefaultUser.js +6 -14
  73. package/dist/utils/authMethods.d.ts +31 -0
  74. package/dist/utils/authMethods.js +44 -0
  75. package/dist/utils/claimUnverifiedAccount.d.ts +35 -0
  76. package/dist/utils/claimUnverifiedAccount.js +43 -0
  77. package/dist/utils/clientAddress.d.ts +26 -0
  78. package/dist/utils/clientAddress.js +9 -0
  79. package/dist/utils/createGenerateVerificationCode.d.ts +4 -0
  80. package/dist/utils/createGenerateVerificationCode.js +12 -2
  81. package/dist/utils/createGenerateWorkspaceInviteToken.js +3 -0
  82. package/dist/utils/createGetApiKeyByAccessKeyId.d.ts +5 -1
  83. package/dist/utils/createGetApiKeyByAccessKeyId.js +5 -0
  84. package/dist/utils/createGetUserByAccessKeyId.d.ts +3 -1
  85. package/dist/utils/createGetUserByEmail.d.ts +3 -1
  86. package/dist/utils/createGetUserByGitHubEmails.d.ts +41 -0
  87. package/dist/utils/createGetUserByGitHubEmails.js +31 -0
  88. package/dist/utils/createGetUserByWorkspaceAccessKeyId.d.ts +3 -1
  89. package/dist/utils/createIsValidVerificationCode.d.ts +5 -0
  90. package/dist/utils/createIsValidVerificationCode.js +34 -7
  91. package/dist/utils/createSendEmail.d.ts +2 -1
  92. package/dist/utils/createSendEmail.js +17 -0
  93. package/dist/utils/createUpdateMembershipStatus.js +10 -3
  94. package/dist/utils/generateOneTimePassword.d.ts +3 -0
  95. package/dist/utils/generateOneTimePassword.js +25 -0
  96. package/dist/utils/getGitHubUserWithOauth2Code.d.ts +20 -2
  97. package/dist/utils/getGitHubUserWithOauth2Code.js +41 -9
  98. package/dist/utils/hasAccessToResource.js +4 -5
  99. package/dist/utils/isWorkspaceAdminCall.d.ts +8 -0
  100. package/dist/utils/isWorkspaceAdminCall.js +47 -0
  101. package/dist/utils/normalizeContact.d.ts +22 -0
  102. package/dist/utils/normalizeContact.js +28 -0
  103. package/dist/utils/passwords.d.ts +11 -0
  104. package/dist/utils/passwords.js +84 -0
  105. package/dist/utils/rateLimiter.d.ts +20 -0
  106. package/dist/utils/rateLimiter.js +108 -0
  107. package/dist/utils/signInThrottle.d.ts +47 -0
  108. package/dist/utils/signInThrottle.js +90 -0
  109. package/dist/utils/userPasswords.d.ts +25 -0
  110. package/dist/utils/userPasswords.js +98 -0
  111. package/dist/utils/withAccess.js +3 -2
  112. package/dist/verification/createSendVerificationCode.js +27 -1
  113. package/dist/verification/createVerifyCode.js +10 -0
  114. package/dist/workspaces/createCreateWorkspace.d.ts +7 -1
  115. package/dist/workspaces/createCreateWorkspace.js +41 -5
  116. package/dist/workspaces/createDeleteWorkspace.d.ts +2 -1
  117. package/dist/workspaces/createDeleteWorkspace.js +16 -2
  118. package/dist/workspaces/createInviteUserToWorkspace.js +19 -17
  119. package/dist/workspaces/createResendWorkspaceMembershipInvitation.js +17 -2
  120. package/dist/workspaces/createUpdateWorkspace.js +18 -4
  121. package/dist/workspaces/createWorkspaceDirectory.d.ts +29 -0
  122. package/dist/workspaces/createWorkspaceDirectory.js +42 -0
  123. package/dist/workspaces/index.d.ts +1 -0
  124. package/dist/workspaces/index.js +1 -0
  125. package/package.json +11 -8
  126. package/dist/generated/@prisma/client/libquery_engine-darwin-arm64.dylib.node +0 -0
  127. package/dist/generated/@prisma/client/runtime/wasm.js +0 -35
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.identityServiceConfigSchema = void 0;
4
4
  exports.loadConfig = loadConfig;
5
+ exports.loadConfigFile = loadConfigFile;
5
6
  /**
6
7
  * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
7
8
  * http://github.com/fonoster/fonoster
@@ -20,135 +21,99 @@ exports.loadConfig = loadConfig;
20
21
  * See the License for the specific language governing permissions and
21
22
  * limitations under the License.
22
23
  */
23
- const fs_1 = require("fs");
24
- const path_1 = require("path");
24
+ const common_1 = require("@fonoster/common");
25
25
  const zod_1 = require("zod");
26
- const zod_validation_error_1 = require("zod-validation-error");
26
+ const config_1 = require("../config");
27
27
  /**
28
- * Configuration for the standalone Identity service.
29
- *
30
- * The service is configured exclusively from a JSON configuration file — no
31
- * environment variables. The file path is taken from a `--config <path>` CLI
32
- * flag, defaulting to `./config/identity.json` relative to the working
33
- * directory.
28
+ * Configuration for the standalone Identity service: identity.json (or
29
+ * .jsonc/.yaml), found via --config, /opt/fonoster or ./config. Fields sit at
30
+ * the root of the file, as they always have; the same settings live under
31
+ * fonoster.yaml's `identity:` section when Identity is embedded in the API
32
+ * server. No environment variables, except ${VAR} in the database URL shared
33
+ * with the Postgres container.
34
34
  */
35
- const keysSchema = zod_1.z
36
- .object({
37
- privateKey: zod_1.z.string().optional(),
38
- publicKey: zod_1.z.string().optional(),
39
- privateKeyPath: zod_1.z.string().optional(),
40
- publicKeyPath: zod_1.z.string().optional()
41
- })
42
- .refine((k) => (k.privateKey || k.privateKeyPath) && (k.publicKey || k.publicKeyPath), {
43
- message: "keys must provide a private and public key, inline or by path"
44
- });
45
- const smtpSchema = zod_1.z.object({
46
- host: zod_1.z.string(),
47
- port: zod_1.z.number(),
48
- secure: zod_1.z.boolean().default(false),
49
- sender: zod_1.z.string(),
50
- auth: zod_1.z
51
- .object({ user: zod_1.z.string().default(""), pass: zod_1.z.string().default("") })
52
- .default({})
53
- });
54
35
  const identityServiceConfigSchema = zod_1.z.object({
55
36
  server: zod_1.z
56
37
  .object({
57
38
  bindAddr: zod_1.z.string().default("0.0.0.0:50051"),
58
- httpBridgePort: zod_1.z.number().default(9000)
39
+ httpBridgePort: zod_1.z.number().int().positive().default(9000)
59
40
  })
60
41
  .default({}),
42
+ logs: common_1.logsSchema,
61
43
  database: zod_1.z.object({ url: zod_1.z.string().min(1) }),
62
- encryptionKey: zod_1.z.string().min(1),
44
+ encryptionKey: config_1.encryptionKeySchema,
63
45
  issuer: zod_1.z.string().min(1),
64
- audience: zod_1.z.string().min(1),
65
- keys: keysSchema,
66
- tokens: zod_1.z
67
- .object({
68
- accessTokenExpiresIn: zod_1.z.string().default("15m"),
69
- refreshTokenExpiresIn: zod_1.z.string().default("30d"),
70
- idTokenExpiresIn: zod_1.z.string().default("15m")
71
- })
72
- .default({}),
73
- security: zod_1.z
74
- .object({
75
- contactVerificationRequired: zod_1.z.boolean().default(false),
76
- twoFactorAuthenticationRequired: zod_1.z.boolean().default(false)
77
- })
78
- .default({}),
79
- invite: zod_1.z
80
- .object({
81
- url: zod_1.z.string().default(""),
82
- failUrl: zod_1.z.string().default(""),
83
- expiration: zod_1.z.string().default("2d")
84
- })
85
- .default({}),
86
- appUrl: zod_1.z.string().default(""),
87
- smtp: smtpSchema.optional(),
88
- oauth2: zod_1.z
89
- .object({
90
- github: zod_1.z.object({ clientId: zod_1.z.string(), clientSecret: zod_1.z.string() })
46
+ audience: zod_1.z.string().min(1).default("api"),
47
+ /** Where the accept-invite bridge redirects. Defaults to the issuer. */
48
+ appUrl: zod_1.z.string().min(1).optional(),
49
+ keys: config_1.keysSchema,
50
+ tokens: config_1.tokensSchema,
51
+ security: config_1.securitySchema,
52
+ invite: config_1.invitesSchema,
53
+ smtp: config_1.smtpSchema
54
+ .omit({ user: true, password: true })
55
+ .extend({
56
+ auth: zod_1.z
57
+ .object({ user: zod_1.z.string().default(""), pass: zod_1.z.string().default("") })
58
+ .default({})
91
59
  })
92
60
  .optional(),
93
- defaultUser: zod_1.z
94
- .object({
95
- name: zod_1.z.string(),
96
- email: zod_1.z.string().email(),
97
- password: zod_1.z.string()
98
- })
99
- .optional()
61
+ twilio: config_1.twilioSchema.optional(),
62
+ oauth2: config_1.oauth2Schema,
63
+ /** Sign-in methods and sign-up policy (`features.auth` in fonoster.yaml). */
64
+ auth: config_1.authFeaturesSchema,
65
+ defaultUser: config_1.ownerSchema
100
66
  });
101
67
  exports.identityServiceConfigSchema = identityServiceConfigSchema;
102
- function configPathFromArgv(argv) {
103
- const i = argv.indexOf("--config");
104
- const fromFlag = i >= 0 ? argv[i + 1] : undefined;
105
- return (0, path_1.resolve)(process.cwd(), fromFlag !== null && fromFlag !== void 0 ? fromFlag : "./config/identity.json");
106
- }
107
- function readKey(inline, path) {
108
- return inline !== null && inline !== void 0 ? inline : (0, fs_1.readFileSync)((0, path_1.resolve)(process.cwd(), path), "utf8");
109
- }
110
- /** Maps the file config into the `IdentityConfig` shape `buildIdentityService` expects. */
111
- function toIdentityConfig(c) {
112
- var _a;
113
- return {
114
- dbUrl: c.database.url,
115
- issuer: c.issuer,
116
- audience: c.audience,
117
- privateKey: readKey(c.keys.privateKey, c.keys.privateKeyPath),
118
- publicKey: readKey(c.keys.publicKey, c.keys.publicKeyPath),
119
- encryptionKey: c.encryptionKey,
120
- accessTokenExpiresIn: c.tokens.accessTokenExpiresIn,
121
- refreshTokenExpiresIn: c.tokens.refreshTokenExpiresIn,
122
- idTokenExpiresIn: c.tokens.idTokenExpiresIn,
123
- workspaceInviteExpiration: c.invite.expiration,
124
- workspaceInviteUrl: c.invite.url,
125
- workspaceInviteFailUrl: c.invite.failUrl,
126
- contactVerificationRequired: c.security.contactVerificationRequired,
127
- twoFactorAuthenticationRequired: c.security.twoFactorAuthenticationRequired,
128
- smtpConfig: c.smtp,
129
- githubOauth2Config: (_a = c.oauth2) === null || _a === void 0 ? void 0 : _a.github
130
- };
68
+ /** Reads and validates the file only (no keys read), e.g. for provisioning. */
69
+ function loadConfigFile(argv = process.argv) {
70
+ const config = (0, common_1.loadServiceConfig)({
71
+ service: "identity",
72
+ schema: identityServiceConfigSchema,
73
+ argv,
74
+ interpolatePaths: ["*"]
75
+ });
76
+ return { config, databaseUrl: config.database.url };
131
77
  }
132
78
  /** Loads and validates the service configuration from the file, failing fast. */
133
79
  function loadConfig(argv = process.argv) {
134
- const path = configPathFromArgv(argv);
135
- let raw;
136
- try {
137
- raw = JSON.parse((0, fs_1.readFileSync)(path, "utf8"));
138
- }
139
- catch (e) {
140
- throw new Error(`unable to read Identity config at ${path}: ${e.message}`);
141
- }
142
- const result = identityServiceConfigSchema.safeParse(raw);
143
- if (!result.success) {
144
- throw new Error(`invalid Identity config at ${path}: ${(0, zod_validation_error_1.fromError)(result.error).toString()}`);
145
- }
146
- const config = result.data;
80
+ var _a;
81
+ const { config, databaseUrl } = loadConfigFile(argv);
82
+ const appUrl = (_a = config.appUrl) !== null && _a !== void 0 ? _a : config.issuer;
83
+ const smtp = config.smtp;
147
84
  return {
148
85
  bindAddr: config.server.bindAddr,
149
86
  httpBridgePort: config.server.httpBridgePort,
150
- appUrl: config.appUrl,
87
+ appUrl,
88
+ logs: config.logs,
89
+ databaseUrl,
151
90
  defaultUser: config.defaultUser,
152
- identityConfig: toIdentityConfig(config)
91
+ identityConfig: (0, config_1.toIdentityConfig)({
92
+ identity: {
93
+ issuer: config.issuer,
94
+ audience: config.audience,
95
+ keys: config.keys,
96
+ tokens: config.tokens,
97
+ security: config.security,
98
+ invites: config.invite,
99
+ oauth2: config.oauth2,
100
+ owner: config.defaultUser
101
+ },
102
+ appUrl,
103
+ databaseUrl,
104
+ encryptionKey: config.encryptionKey,
105
+ smtp: smtp
106
+ ? {
107
+ host: smtp.host,
108
+ port: smtp.port,
109
+ secure: smtp.secure,
110
+ sender: smtp.sender,
111
+ user: smtp.auth.user,
112
+ password: smtp.auth.pass
113
+ }
114
+ : undefined,
115
+ twilio: config.twilio,
116
+ auth: config.auth
117
+ })
153
118
  };
154
119
  }
@@ -72,18 +72,20 @@ const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filena
72
72
  * Standalone Identity gRPC service. Wraps `buildIdentityService` with the auth
73
73
  * interceptor, the identity allow-list, a health service, and the accept-invite
74
74
  * HTTP bridge — without any telephony subsystem. Configured entirely from a
75
- * file (see `./config`); no environment variables.
75
+ * file (see `./config`): identity.json, fields at the root.
76
76
  */
77
77
  function main() {
78
78
  return __awaiter(this, void 0, void 0, function* () {
79
- const { bindAddr, httpBridgePort, appUrl, defaultUser, identityConfig } = (0, config_1.loadConfig)();
79
+ const { bindAddr, httpBridgePort, appUrl, logs, defaultUser, identityConfig } = (0, config_1.loadConfig)();
80
+ (0, logger_1.configureLogger)(logs);
80
81
  const { definition, handlers } = (0, __1.buildIdentityService)(identityConfig);
81
- const authorization = (0, common_1.createAuthInterceptor)(identityConfig.publicKey, __1.identityAllowList);
82
+ const authorization = (0, common_1.createAuthInterceptor)(identityConfig.publicKey, __1.identityAllowList, { issuer: identityConfig.issuer, audience: identityConfig.audience });
82
83
  const credentials = yield (0, common_1.getServerCredentials)({});
83
84
  const healthImpl = new grpc_health_check_1.HealthImplementation(common_1.statusMap);
84
85
  const server = new grpc.Server({ interceptors: [authorization] });
85
86
  healthImpl.addToServer(server);
86
87
  server.addService((0, common_1.createServiceDefinition)(definition), handlers);
88
+ yield (0, __1.migratePasswordStorage)(identityConfig);
87
89
  if (defaultUser) {
88
90
  yield (0, __1.upsertDefaultUser)(identityConfig, defaultUser);
89
91
  }
package/dist/service.d.ts CHANGED
@@ -86,6 +86,12 @@ declare function buildIdentityService(identityConfig: IdentityConfig): {
86
86
  exchangeRefreshToken: (call: {
87
87
  request: unknown;
88
88
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
89
+ exchangeVerificationCode: (call: {
90
+ request: unknown;
91
+ }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
92
+ revokeToken: (call: {
93
+ request: unknown;
94
+ }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
89
95
  getPublicKey: (_: unknown, callback: (error: import("@fonoster/common").GrpcErrorMessage, response?: {
90
96
  publicKey: string;
91
97
  }) => void) => Promise<void>;
package/dist/service.js CHANGED
@@ -23,6 +23,7 @@ exports.buildIdentityService = buildIdentityService;
23
23
  const db_1 = require("./db");
24
24
  const createExchangeOauth2Code_1 = require("./exchanges/createExchangeOauth2Code");
25
25
  const getPublicKey_1 = require("./getPublicKey");
26
+ const authMethods_1 = require("./utils/authMethods");
26
27
  const verification_1 = require("./verification");
27
28
  const _1 = require(".");
28
29
  const serviceDefinitionParams = {
@@ -33,13 +34,16 @@ const serviceDefinitionParams = {
33
34
  };
34
35
  exports.serviceDefinitionParams = serviceDefinitionParams;
35
36
  function buildIdentityService(identityConfig) {
37
+ var _a, _b;
36
38
  const prisma = (0, db_1.createPrismaClient)(identityConfig.dbUrl, identityConfig.encryptionKey);
39
+ // The control plane's regions read workspaces through this (add-private-regions PR-14).
40
+ (_b = (_a = identityConfig.regions) === null || _a === void 0 ? void 0 : _a.attachDirectory) === null || _b === void 0 ? void 0 : _b.call(_a, (0, _1.createWorkspaceDirectory)(prisma));
37
41
  const service = {
38
42
  definition: serviceDefinitionParams,
39
43
  handlers: {
40
44
  // Workspace operations
41
- createWorkspace: (0, _1.createCreateWorkspace)(prisma),
42
- deleteWorkspace: (0, _1.createDeleteWorkspace)(prisma),
45
+ createWorkspace: (0, _1.createCreateWorkspace)(prisma, identityConfig.regions),
46
+ deleteWorkspace: (0, _1.createDeleteWorkspace)(prisma, identityConfig.regions),
43
47
  getWorkspace: (0, _1.createGetWorkspace)(prisma),
44
48
  updateWorkspace: (0, _1.createUpdateWorkspace)(prisma),
45
49
  listWorkspaces: (0, _1.createListWorkspaces)(prisma),
@@ -48,7 +52,7 @@ function buildIdentityService(identityConfig) {
48
52
  resendWorkspaceMembershipInvitation: (0, _1.createResendWorkspaceMembershipInvitation)(prisma, identityConfig, _1.sendInvite),
49
53
  removeUserFromWorkspace: (0, _1.createRemoveUserFromWorkspace)(prisma),
50
54
  // User operations
51
- createUser: (0, _1.createCreateUser)(prisma),
55
+ createUser: (0, _1.createCreateUser)(prisma, identityConfig),
52
56
  createUserWithOauth2Code: (0, _1.createCreateUserWithOauth2Code)(prisma, identityConfig),
53
57
  getUser: (0, _1.createGetUser)(prisma),
54
58
  deleteUser: (0, _1.createDeleteUser)(prisma),
@@ -65,6 +69,8 @@ function buildIdentityService(identityConfig) {
65
69
  exchangeCredentials: (0, _1.createExchangeCredentials)(prisma, identityConfig),
66
70
  exchangeOauth2Code: (0, createExchangeOauth2Code_1.createExchangeOauth2Code)(prisma, identityConfig),
67
71
  exchangeRefreshToken: (0, _1.createExchangeRefreshToken)(prisma, identityConfig),
72
+ exchangeVerificationCode: (0, _1.createExchangeVerificationCode)(prisma, identityConfig),
73
+ revokeToken: (0, _1.createRevokeToken)(prisma, identityConfig),
68
74
  getPublicKey: (0, getPublicKey_1.createGetPublicKey)(identityConfig.publicKey),
69
75
  // Placeholders for conditional handlers
70
76
  sendVerificationCode: undefined,
@@ -72,7 +78,8 @@ function buildIdentityService(identityConfig) {
72
78
  }
73
79
  };
74
80
  if (identityConfig.contactVerificationRequired ||
75
- identityConfig.twoFactorAuthenticationRequired) {
81
+ identityConfig.twoFactorAuthenticationRequired ||
82
+ (0, authMethods_1.isMethodEnabled)(identityConfig, "email_code")) {
76
83
  service.handlers.sendVerificationCode = (0, verification_1.createSendVerificationCode)(prisma, identityConfig);
77
84
  service.handlers.verifyCode = (0, verification_1.createVerifyCode)(prisma);
78
85
  }
@@ -18,7 +18,12 @@
18
18
  */
19
19
  import { GrpcErrorMessage } from "@fonoster/common";
20
20
  import { Prisma } from "../db";
21
- declare function createCreateUser(prisma: Prisma): (call: {
21
+ import { IdentityConfig } from "../exchanges/types";
22
+ /**
23
+ * Self-service sign-up with a password. Allowed only when sign-up is open and
24
+ * password sign-in is enabled; invited users are created by the invite.
25
+ */
26
+ declare function createCreateUser(prisma: Prisma, identityConfig: IdentityConfig): (call: {
22
27
  request: unknown;
23
28
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
24
29
  export { createCreateUser };
@@ -30,22 +30,30 @@ exports.createCreateUser = createCreateUser;
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 authMethods_1 = require("../utils/authMethods");
33
35
  const generateAccessKeyId_1 = require("../utils/generateAccessKeyId");
36
+ const userPasswords_1 = require("../utils/userPasswords");
34
37
  const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
35
- function createCreateUser(prisma) {
38
+ /**
39
+ * Self-service sign-up with a password. Allowed only when sign-up is open and
40
+ * password sign-in is enabled; invited users are created by the invite.
41
+ */
42
+ function createCreateUser(prisma, identityConfig) {
36
43
  const createUser = (call, callback) => __awaiter(this, void 0, void 0, function* () {
37
44
  const { request } = call;
38
45
  const { name, email, password, avatar, phone } = request;
39
46
  logger.verbose("call to createUser", { email });
47
+ if (!(0, authMethods_1.isOpenSignup)(identityConfig) ||
48
+ !(0, authMethods_1.isMethodEnabled)(identityConfig, "password")) {
49
+ return callback({
50
+ code: grpc_js_1.status.PERMISSION_DENIED,
51
+ message: "Sign-up is by invitation only"
52
+ });
53
+ }
40
54
  const user = yield prisma.user.create({
41
- data: {
42
- name,
43
- email,
44
- accessKeyId: (0, generateAccessKeyId_1.generateAccessKeyId)(generateAccessKeyId_1.AccessKeyIdType.USER),
45
- password,
46
- avatar,
47
- phoneNumber: phone || undefined
48
- }
55
+ data: Object.assign(Object.assign({ name,
56
+ email, accessKeyId: (0, generateAccessKeyId_1.generateAccessKeyId)(generateAccessKeyId_1.AccessKeyIdType.USER) }, (yield (0, userPasswords_1.passwordColumns)(password))), { avatar, phoneNumber: phone || undefined })
49
57
  });
50
58
  const { ref } = user;
51
59
  callback(null, { ref });
@@ -64,45 +64,49 @@ exports.createCreateUserWithOauth2Code = createCreateUserWithOauth2Code;
64
64
  const common_1 = require("@fonoster/common");
65
65
  const logger_1 = require("@fonoster/logger");
66
66
  const grpc = __importStar(require("@grpc/grpc-js"));
67
- const nanoid_1 = require("nanoid");
68
67
  const exchangeTokens_1 = require("../exchanges/exchangeTokens");
69
- const createGetUserByEmail_1 = require("../utils/createGetUserByEmail");
70
- const generateAccessKeyId_1 = require("../utils/generateAccessKeyId");
68
+ const authMethods_1 = require("../utils/authMethods");
69
+ const createGetUserByGitHubEmails_1 = require("../utils/createGetUserByGitHubEmails");
71
70
  const getGitHubUserWithOauth2Code_1 = require("../utils/getGitHubUserWithOauth2Code");
71
+ const createUserFromGitHub_1 = require("./createUserFromGitHub");
72
72
  const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
73
73
  function createCreateUserWithOauth2Code(prisma, identityConfig) {
74
74
  const createUserWithOauth2Code = (call, callback) => __awaiter(this, void 0, void 0, function* () {
75
+ var _a;
75
76
  const { request } = call;
76
77
  const { code } = request;
77
78
  logger.verbose("call to createCreateUserWithOauth2Code");
79
+ if (!(0, authMethods_1.isMethodEnabled)(identityConfig, "github") ||
80
+ !identityConfig.githubOauth2Config) {
81
+ return callback((0, authMethods_1.methodDisabledError)("github"));
82
+ }
83
+ if (!(0, authMethods_1.isOpenSignup)(identityConfig)) {
84
+ return callback({
85
+ code: grpc.status.PERMISSION_DENIED,
86
+ message: "Sign-up is by invitation only"
87
+ });
88
+ }
78
89
  const userData = yield (0, getGitHubUserWithOauth2Code_1.getGitHubUserWithOauth2Code)({
79
90
  clientId: identityConfig.githubOauth2Config.clientId,
80
91
  clientSecret: identityConfig.githubOauth2Config.clientSecret,
92
+ url: identityConfig.githubOauth2Config.url,
93
+ apiUrl: identityConfig.githubOauth2Config.apiUrl,
81
94
  code
82
95
  });
83
96
  if (!userData.email) {
84
97
  return callback({
85
98
  code: grpc.status.PERMISSION_DENIED,
86
- message: "Failed to get user data from GitHub. This typically happens when your GitHub account doesn't have a public email address"
99
+ message: "Your GitHub account has no verified primary email"
87
100
  });
88
101
  }
89
- const userFromDB = yield (0, createGetUserByEmail_1.createGetUserByEmail)(prisma)(userData.email);
102
+ const userFromDB = yield (0, createGetUserByGitHubEmails_1.createGetUserByGitHubEmails)(prisma)((_a = userData.verifiedEmails) !== null && _a !== void 0 ? _a : [userData.email]);
90
103
  if (userFromDB) {
91
104
  return callback({
92
105
  code: grpc.status.ALREADY_EXISTS,
93
106
  message: "User already exists"
94
107
  });
95
108
  }
96
- const user = yield prisma.user.create({
97
- data: {
98
- name: userData.name,
99
- email: userData.email,
100
- accessKeyId: (0, generateAccessKeyId_1.generateAccessKeyId)(generateAccessKeyId_1.AccessKeyIdType.USER),
101
- emailVerified: true,
102
- password: (0, nanoid_1.customAlphabet)("1234567890abcdef", 10)(),
103
- avatar: userData.avatar_url
104
- }
105
- });
109
+ const user = yield (0, createUserFromGitHub_1.createUserFromGitHub)(prisma)(Object.assign(Object.assign({}, userData), { email: userData.email }));
106
110
  callback(null, yield (0, exchangeTokens_1.exchangeTokens)(prisma, identityConfig)(user.accessKeyId));
107
111
  });
108
112
  return (0, common_1.withErrorHandlingAndValidation)(createUserWithOauth2Code, common_1.Validators.createUserWithOauth2CodeRequestSchema);
@@ -18,6 +18,11 @@
18
18
  */
19
19
  import { GrpcErrorMessage } from "@fonoster/common";
20
20
  import { Prisma } from "../db";
21
+ /**
22
+ * Sets a new password with an emailed code and signs the user out everywhere
23
+ * (their refresh tokens are revoked). Attempts are rate limited per email and
24
+ * per client IP, in this process's memory.
25
+ */
21
26
  declare function createResetPassword(prisma: Prisma): (call: {
22
27
  request: unknown;
23
28
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
@@ -32,18 +32,43 @@ 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 constants_1 = require("../constants");
36
+ const refreshTokens_1 = require("../exchanges/refreshTokens");
37
+ const clientAddress_1 = require("../utils/clientAddress");
35
38
  const createIsValidVerificationCode_1 = require("../utils/createIsValidVerificationCode");
39
+ const rateLimiter_1 = require("../utils/rateLimiter");
40
+ const userPasswords_1 = require("../utils/userPasswords");
36
41
  const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
42
+ /**
43
+ * Sets a new password with an emailed code and signs the user out everywhere
44
+ * (their refresh tokens are revoked). Attempts are rate limited per email and
45
+ * per client IP, in this process's memory.
46
+ */
37
47
  function createResetPassword(prisma) {
38
48
  const isValidVerificationCode = (0, createIsValidVerificationCode_1.createIsValidVerificationCode)(prisma);
49
+ const refreshTokens = (0, refreshTokens_1.createRefreshTokenStore)(prisma);
50
+ const { perEmail, perIp, windowMs } = constants_1.RESET_PASSWORD_ATTEMPT_LIMITS;
51
+ const withinEmailLimit = (0, rateLimiter_1.createSharedRateLimiter)(prisma, {
52
+ name: "reset-password:contact",
53
+ limit: perEmail,
54
+ windowMs
55
+ });
56
+ const withinIpLimit = (0, rateLimiter_1.createSharedRateLimiter)(prisma, {
57
+ name: "reset-password:ip",
58
+ limit: perIp,
59
+ windowMs
60
+ });
39
61
  const resetPassword = (call, callback) => __awaiter(this, void 0, void 0, function* () {
40
62
  const { request } = call;
41
63
  const { username, password, verificationCode } = request;
42
- logger.verbose("call to resetPassword", {
43
- username,
44
- password,
45
- verificationCode
46
- });
64
+ logger.verbose("call to resetPassword", { username });
65
+ if (!(yield withinIpLimit((0, clientAddress_1.clientAddress)(call))) ||
66
+ !(yield withinEmailLimit(username.toLowerCase()))) {
67
+ return callback({
68
+ code: grpc_js_1.status.RESOURCE_EXHAUSTED,
69
+ message: "Too many password reset attempts; try again later"
70
+ });
71
+ }
47
72
  const isValid = yield isValidVerificationCode({
48
73
  type: types_1.ContactType.EMAIL,
49
74
  value: username,
@@ -55,10 +80,11 @@ function createResetPassword(prisma) {
55
80
  message: "Invalid verification code"
56
81
  });
57
82
  }
58
- yield prisma.user.update({
83
+ const user = yield prisma.user.update({
59
84
  where: { email: username },
60
- data: { password }
85
+ data: Object.assign(Object.assign({}, (yield (0, userPasswords_1.passwordColumns)(password))), { mustChangePassword: false })
61
86
  });
87
+ yield refreshTokens.revokeAll(user.accessKeyId);
62
88
  callback(null);
63
89
  });
64
90
  return (0, common_1.withErrorHandlingAndValidation)(resetPassword, common_1.Validators.resetPasswordRequestSchema);
@@ -19,6 +19,12 @@
19
19
  import { GrpcErrorMessage } from "@fonoster/common";
20
20
  import { Prisma } from "../db";
21
21
  import { IdentityConfig } from "../exchanges/types";
22
+ /**
23
+ * Emails a reset link. The link always points at Identity's configured reset
24
+ * page (`resetPasswordUrl`); a URL in the request is ignored. Sends are rate
25
+ * limited per email and per client IP, in this process's memory (each replica
26
+ * counts on its own).
27
+ */
22
28
  declare function createSendResetPasswordCode(prisma: Prisma, identityConfig: IdentityConfig): (call: {
23
29
  request: unknown;
24
30
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;
@@ -31,16 +31,48 @@ exports.createSendResetPasswordCode = createSendResetPasswordCode;
31
31
  const common_1 = require("@fonoster/common");
32
32
  const logger_1 = require("@fonoster/logger");
33
33
  const types_1 = require("@fonoster/types");
34
+ const grpc_js_1 = require("@grpc/grpc-js");
35
+ const constants_1 = require("../constants");
34
36
  const utils_1 = require("../utils");
37
+ const clientAddress_1 = require("../utils/clientAddress");
35
38
  const createGenerateVerificationCode_1 = require("../utils/createGenerateVerificationCode");
39
+ const rateLimiter_1 = require("../utils/rateLimiter");
36
40
  const sendResetPasswordEmail_1 = require("./sendResetPasswordEmail");
37
41
  const logger = (0, logger_1.getLogger)({ service: "identity", filePath: __filename });
42
+ const tooManyRequests = {
43
+ code: grpc_js_1.status.RESOURCE_EXHAUSTED,
44
+ message: "Too many password reset requests; try again later"
45
+ };
46
+ /**
47
+ * Emails a reset link. The link always points at Identity's configured reset
48
+ * page (`resetPasswordUrl`); a URL in the request is ignored. Sends are rate
49
+ * limited per email and per client IP, in this process's memory (each replica
50
+ * counts on its own).
51
+ */
38
52
  function createSendResetPasswordCode(prisma, identityConfig) {
53
+ var _a;
39
54
  const generateVerificationCode = (0, createGenerateVerificationCode_1.createGenerateVerificationCode)(prisma);
55
+ const { perEmail, perIp, windowMs } = constants_1.VERIFICATION_CODE_SEND_LIMITS;
56
+ const withinEmailLimit = (0, rateLimiter_1.createSharedRateLimiter)(prisma, {
57
+ name: "reset-code:contact",
58
+ limit: perEmail,
59
+ windowMs
60
+ });
61
+ const withinIpLimit = (0, rateLimiter_1.createSharedRateLimiter)(prisma, {
62
+ name: "reset-code:ip",
63
+ limit: perIp,
64
+ windowMs
65
+ });
66
+ const resetPasswordUrl = (_a = identityConfig.resetPasswordUrl) !== null && _a !== void 0 ? _a : `${identityConfig.issuer.replace(/\/+$/, "")}/reset-password`;
40
67
  const sendResetPasswordCode = (call, callback) => __awaiter(this, void 0, void 0, function* () {
41
68
  const { request } = call;
42
69
  const { username } = request;
43
70
  logger.verbose("call to sendResetPasswordCode", { username });
71
+ // Counted before the lookup, so the answer is the same for unknown emails.
72
+ if (!(yield withinIpLimit((0, clientAddress_1.clientAddress)(call))) ||
73
+ !(yield withinEmailLimit(username.toLowerCase()))) {
74
+ return callback(tooManyRequests);
75
+ }
44
76
  const user = yield prisma.user.findUnique({
45
77
  where: { email: username }
46
78
  });
@@ -57,7 +89,7 @@ function createSendResetPasswordCode(prisma, identityConfig) {
57
89
  const encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64");
58
90
  yield (0, sendResetPasswordEmail_1.sendResetPasswordEmail)((0, utils_1.createSendEmail)(identityConfig), {
59
91
  recipient: username,
60
- resetPasswordUrl: `${request.resetPasswordUrl}?token=${encodedPayload}`
92
+ resetPasswordUrl: `${resetPasswordUrl}?token=${encodeURIComponent(encodedPayload)}`
61
93
  });
62
94
  callback(null);
63
95
  });
@@ -18,6 +18,13 @@
18
18
  */
19
19
  import { GrpcErrorMessage } from "@fonoster/common";
20
20
  import { Prisma } from "../db";
21
+ /**
22
+ * Updates the caller's own profile. A new password needs the current one
23
+ * (an access token alone can't change it) and signs the user out everywhere:
24
+ * every refresh token is revoked. Accounts without a password (GitHub or
25
+ * email-code sign-in) set one with the password-reset flow, which proves the
26
+ * email.
27
+ */
21
28
  declare function createUpdateUser(prisma: Prisma): (call: {
22
29
  request: unknown;
23
30
  }, callback: (error?: import("@fonoster/common").GrpcErrorMessage, response?: unknown) => void) => Promise<void>;