@supertokens-plugins/rownd-nodejs 0.5.0 → 0.6.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/dist/index.mjs CHANGED
@@ -616,12 +616,11 @@ function createLoggerConfig() {
616
616
  }
617
617
  var config = createLoggerConfig();
618
618
  var logger = common_default(config);
619
- var formatters = logger.formatters;
620
- if (formatters) {
621
- formatters.o = function(value) {
619
+ if (logger.formatters) {
620
+ logger.formatters.o = function(value) {
622
621
  return util.inspect(value, { colors: this.useColors, compact: true }).split("\n").map((line) => line.trim()).join(" ");
623
622
  };
624
- formatters.O = function(value) {
623
+ logger.formatters.O = function(value) {
625
624
  return util.inspect(value, { colors: this.useColors, compact: false });
626
625
  };
627
626
  }
@@ -843,6 +842,10 @@ var RowndPluginError = class extends Error {
843
842
  };
844
843
 
845
844
  // src/utils.ts
845
+ var ROWND_OAUTH_LOGIN_CHALLENGE_PARAM = "rownd_oauth_login_challenge";
846
+ function resolveTenantId(req) {
847
+ return req.getKeyValueFromQuery("tenantId") || PUBLIC_TENANT_ID;
848
+ }
846
849
  function rewriteLinkPath(inputUrl, targetPath, searchParams) {
847
850
  try {
848
851
  const url = new URL(inputUrl);
@@ -1043,7 +1046,8 @@ function getMagicLinkBootstrapParams(input) {
1043
1046
  ...input.appVariantId ? { appVariantId: input.appVariantId } : {},
1044
1047
  ...input.displayContext ? { displayContext: input.displayContext } : {},
1045
1048
  ...input.redirectToPath ? { redirectToPath: input.redirectToPath } : {},
1046
- ...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {}
1049
+ ...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {},
1050
+ ...input.oauthLoginChallenge ? { oauthLoginChallenge: input.oauthLoginChallenge } : {}
1047
1051
  };
1048
1052
  }
1049
1053
  function rewriteMagicLink(input) {
@@ -1069,6 +1073,10 @@ function getRequestedRedirectToPathFromRequest(req) {
1069
1073
  const redirectToPath = req.getKeyValueFromQuery("rownd_redirect_to_path");
1070
1074
  return typeof redirectToPath === "string" && redirectToPath.length > 0 ? redirectToPath : void 0;
1071
1075
  }
1076
+ function getRequestedOAuthLoginChallengeFromRequest(req) {
1077
+ const loginChallenge = req.getKeyValueFromQuery(ROWND_OAUTH_LOGIN_CHALLENGE_PARAM);
1078
+ return typeof loginChallenge === "string" && loginChallenge.length > 0 ? loginChallenge : void 0;
1079
+ }
1072
1080
 
1073
1081
  // src/config.ts
1074
1082
  var pluginConfig;
@@ -1441,6 +1449,7 @@ function buildAuthMobileConfig(authMobile) {
1441
1449
  }
1442
1450
 
1443
1451
  // src/rownd-compatibility.ts
1452
+ import { createHash } from "crypto";
1444
1453
  import SuperTokens from "supertokens-node";
1445
1454
  import UserMetadata2 from "supertokens-node/recipe/usermetadata";
1446
1455
  var IDENTITY_USER_DATA_FIELDS = /* @__PURE__ */ new Set([
@@ -1454,13 +1463,21 @@ var INTERNAL_METADATA_FIELDS = /* @__PURE__ */ new Set([
1454
1463
  "original_rownd_user",
1455
1464
  "rownd_pending_verification"
1456
1465
  ]);
1466
+ var SUPERTOKENS_FAKE_EMAIL_DOMAIN = "stfakeemail.supertokens.com";
1467
+ function isSuperTokensFakeEmail(email) {
1468
+ return typeof email === "string" && email.toLowerCase().endsWith(`@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`);
1469
+ }
1470
+ function buildSuperTokensFakeEmail(thirdPartyUserId, thirdPartyId) {
1471
+ const hash = createHash("sha256").update(`${thirdPartyId}:${thirdPartyUserId}`).digest("hex").slice(0, 32);
1472
+ return `st-${thirdPartyId}-${hash}@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`;
1473
+ }
1457
1474
  function isIdentityField(field) {
1458
1475
  return IDENTITY_USER_DATA_FIELDS.has(field);
1459
1476
  }
1460
1477
  function isInternalMetadataField(field) {
1461
1478
  return INTERNAL_METADATA_FIELDS.has(field);
1462
1479
  }
1463
- function mapRowndUserToSuperTokens(rowndUser) {
1480
+ function mapRowndUserToSuperTokens(rowndUser, tenantId) {
1464
1481
  const loginMethods = [];
1465
1482
  const rowndUserData = rowndUser.data || {};
1466
1483
  const rowndUserVerifiedData = rowndUser.verified_data || {};
@@ -1468,41 +1485,41 @@ function mapRowndUserToSuperTokens(rowndUser) {
1468
1485
  throw new Error("Rownd user has no user_id");
1469
1486
  }
1470
1487
  if (rowndUserData.google_id) {
1471
- if (!rowndUserData.email) {
1472
- throw new Error("Rownd Google user is missing email");
1473
- }
1488
+ const googleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.google_id, "google");
1474
1489
  loginMethods.push({
1475
1490
  recipeId: "thirdparty",
1476
1491
  thirdPartyId: "google",
1477
1492
  thirdPartyUserId: rowndUserData.google_id,
1478
- email: rowndUserData.email,
1479
- isVerified: !!rowndUserVerifiedData.google_id
1493
+ email: googleEmail,
1494
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id,
1495
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1480
1496
  });
1481
1497
  }
1482
1498
  if (rowndUserData.apple_id) {
1483
- if (!rowndUserData.email) {
1484
- throw new Error("Rownd Apple user is missing email");
1485
- }
1499
+ const appleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.apple_id, "apple");
1486
1500
  loginMethods.push({
1487
1501
  recipeId: "thirdparty",
1488
1502
  thirdPartyId: "apple",
1489
1503
  thirdPartyUserId: rowndUserData.apple_id,
1490
- email: rowndUserData.email,
1491
- isVerified: !!rowndUserVerifiedData.apple_id
1504
+ email: appleEmail,
1505
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id,
1506
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1492
1507
  });
1493
1508
  }
1494
1509
  if (rowndUserData.phone_number) {
1495
1510
  loginMethods.push({
1496
1511
  recipeId: "passwordless",
1497
1512
  phoneNumber: rowndUserData.phone_number,
1498
- isVerified: !!rowndUserVerifiedData.phone_number
1513
+ isVerified: !!rowndUserVerifiedData.phone_number,
1514
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1499
1515
  });
1500
1516
  }
1501
1517
  if (rowndUserData.email && !rowndUserData.google_id && !rowndUserData.apple_id) {
1502
1518
  loginMethods.push({
1503
1519
  recipeId: "passwordless",
1504
1520
  email: rowndUserData.email,
1505
- isVerified: !!rowndUserVerifiedData.email
1521
+ isVerified: !!rowndUserVerifiedData.email,
1522
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1506
1523
  });
1507
1524
  }
1508
1525
  let authLevel = rowndUser.auth_level;
@@ -1514,7 +1531,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
1514
1531
  thirdPartyId,
1515
1532
  thirdPartyUserId: rowndUserData.user_id,
1516
1533
  email: `${rowndUserData.user_id}@anonymous.local`,
1517
- isVerified: false
1534
+ isVerified: false,
1535
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1518
1536
  });
1519
1537
  }
1520
1538
  const userMetadata = buildRowndUserMetadata(rowndUser);
@@ -1673,7 +1691,7 @@ async function buildStandardOAuthClaims(user, scopes) {
1673
1691
  const rowndData = isJsonRecord(metadata.original_rownd_user?.data) ? metadata.original_rownd_user.data : {};
1674
1692
  const verifiedData = isJsonRecord(metadata.original_rownd_user?.verified_data) ? metadata.original_rownd_user.verified_data : {};
1675
1693
  if (scopes.includes("email")) {
1676
- const email = firstString(rowndData.email) ?? user.emails[0];
1694
+ const email = firstRealEmail(firstString(rowndData.email), ...user.emails);
1677
1695
  if (email) {
1678
1696
  claims.email = email;
1679
1697
  claims.email_verified = isOAuthClaimVerified(
@@ -1740,6 +1758,11 @@ function firstString(value) {
1740
1758
  }
1741
1759
  return typeof value === "string" && value.length > 0 ? value : void 0;
1742
1760
  }
1761
+ function firstRealEmail(...values) {
1762
+ return values.find((entry) => {
1763
+ return typeof entry === "string" && entry.length > 0 && !isSuperTokensFakeEmail(entry);
1764
+ });
1765
+ }
1743
1766
  function isOAuthClaimVerified(value, expectedValue, fallback) {
1744
1767
  return value === true || value === expectedValue || fallback;
1745
1768
  }
@@ -2023,6 +2046,7 @@ async function createMagicLinkWithConfirmationBypass(input) {
2023
2046
  })}${getAppInfoString(stConfig.appInfo.websiteBasePath)}/verify?preAuthSessionId=${encodeURIComponent(
2024
2047
  codeInfo.preAuthSessionId
2025
2048
  )}&tenantId=${encodeURIComponent(tenantId)}#${encodeURIComponent(codeInfo.linkCode)}`;
2049
+ const oauthLoginChallenge = userContext.rowndOAuthLoginChallenge;
2026
2050
  const rewrittenUrl = new URL(rewriteMagicLink({
2027
2051
  magicLink,
2028
2052
  clientDomain,
@@ -2033,7 +2057,8 @@ async function createMagicLinkWithConfirmationBypass(input) {
2033
2057
  appVariantId,
2034
2058
  displayContext: input.displayContext,
2035
2059
  redirectToPath,
2036
- clientDomainKey: input.clientDomain
2060
+ clientDomainKey: input.clientDomain,
2061
+ oauthLoginChallenge: typeof oauthLoginChallenge === "string" ? oauthLoginChallenge : void 0
2037
2062
  })
2038
2063
  }));
2039
2064
  rewrittenUrl.searchParams.set(PASSWORDLESS_BYPASS_DEVICE_CONFIRMATION_PARAM, "true");
@@ -2053,7 +2078,7 @@ function getPendingVerifications(metadata) {
2053
2078
  function isPendingVerification(value) {
2054
2079
  return isRecord(value) && typeof value.id === "string" && typeof value.field === "string" && typeof value.value === "string" && typeof value.created_at === "string";
2055
2080
  }
2056
- async function getUserById(userId) {
2081
+ async function getUserById(userId, tenantId = PUBLIC_TENANT_ID) {
2057
2082
  const metadata = await getUserMetadata(userId);
2058
2083
  const stUser = await SuperTokens2.getUser(userId);
2059
2084
  if (!stUser) {
@@ -2082,9 +2107,12 @@ async function getUserById(userId) {
2082
2107
  const verifiedData = {
2083
2108
  ...originalRowndUser?.verified_data || {}
2084
2109
  };
2085
- for (const method of stUser.loginMethods) {
2110
+ const tenantLoginMethods = stUser.loginMethods.filter(
2111
+ (method) => method.tenantIds.includes(tenantId)
2112
+ );
2113
+ for (const method of tenantLoginMethods) {
2086
2114
  if (method.recipeId === "passwordless") {
2087
- if (method.email) {
2115
+ if (method.email && !isSuperTokensFakeEmail(method.email)) {
2088
2116
  verifiedData.email = method.email;
2089
2117
  if (data.email === void 0) data.email = method.email;
2090
2118
  }
@@ -2096,10 +2124,10 @@ async function getUserById(userId) {
2096
2124
  } else if (method.recipeId === "thirdparty") {
2097
2125
  const thirdPartyId = getThirdPartyId(method);
2098
2126
  const thirdPartyUserId = getThirdPartyUserId(method);
2099
- if (method.verified && method.email) {
2127
+ if (method.verified && method.email && !isSuperTokensFakeEmail(method.email)) {
2100
2128
  verifiedData.email = method.email;
2101
2129
  }
2102
- if (method.email && data.email === void 0) {
2130
+ if (method.email && !isSuperTokensFakeEmail(method.email) && data.email === void 0) {
2103
2131
  data.email = method.email;
2104
2132
  }
2105
2133
  if (thirdPartyId === "google" && thirdPartyUserId) {
@@ -2111,7 +2139,7 @@ async function getUserById(userId) {
2111
2139
  verifiedData.apple_id = thirdPartyUserId;
2112
2140
  }
2113
2141
  } else if (method.recipeId === "emailpassword") {
2114
- if (method.email && data.email === void 0) {
2142
+ if (method.email && !isSuperTokensFakeEmail(method.email) && data.email === void 0) {
2115
2143
  data.email = method.email;
2116
2144
  }
2117
2145
  }
@@ -2122,12 +2150,16 @@ async function getUserById(userId) {
2122
2150
  if (verifiedData.phone_number === true && typeof data.phone_number === "string") {
2123
2151
  verifiedData.phone_number = data.phone_number;
2124
2152
  }
2125
- const anonymousId = getAnonymousId(stUser.id, stUser, metadata);
2153
+ const tenantUser = {
2154
+ ...stUser,
2155
+ loginMethods: tenantLoginMethods
2156
+ };
2157
+ const anonymousId = getAnonymousId(stUser.id, tenantUser, metadata);
2126
2158
  if (anonymousId && data.anonymous_id === void 0) {
2127
2159
  data.anonymous_id = anonymousId;
2128
2160
  }
2129
2161
  const authLevel = getEffectiveAuthLevel(
2130
- stUser,
2162
+ tenantUser,
2131
2163
  originalRowndUser?.auth_level,
2132
2164
  verifiedData
2133
2165
  );
@@ -2136,15 +2168,15 @@ async function getUserById(userId) {
2136
2168
  data[key] = "";
2137
2169
  }
2138
2170
  }
2139
- const sortedByJoined = [...stUser.loginMethods].sort(
2171
+ const sortedByJoined = [...tenantLoginMethods].sort(
2140
2172
  (a, b) => a.timeJoined - b.timeJoined
2141
2173
  );
2142
- const latestSessionInfo = await getLatestSessionInfo(stUser.id);
2174
+ const latestSessionInfo = await getLatestSessionInfo(stUser.id, tenantId);
2143
2175
  const firstMethod = sortedByJoined[0];
2144
2176
  const latestSessionRecipeUserId = latestSessionInfo?.recipeUserId.getAsString();
2145
2177
  const lastMethod = latestSessionRecipeUserId ? stUser.loginMethods.find(
2146
2178
  (method) => method.recipeUserId.getAsString() === latestSessionRecipeUserId
2147
- ) : [...stUser.loginMethods].sort((a, b) => b.timeJoined - a.timeJoined)[0];
2179
+ ) : [...tenantLoginMethods].sort((a, b) => b.timeJoined - a.timeJoined)[0];
2148
2180
  const lastSignInAt = latestSessionInfo?.timeCreated ?? stUser.timeJoined;
2149
2181
  const metadataMeta = Object.fromEntries(
2150
2182
  Object.entries(metadata).filter(
@@ -2172,11 +2204,11 @@ async function getUserById(userId) {
2172
2204
  attributes: originalRowndUser?.attributes || {}
2173
2205
  };
2174
2206
  }
2175
- async function getLatestSessionInfo(userId) {
2207
+ async function getLatestSessionInfo(userId, tenantId) {
2176
2208
  const sessionHandles = await Session.getAllSessionHandlesForUser(
2177
2209
  userId,
2178
2210
  true,
2179
- PUBLIC_TENANT_ID
2211
+ tenantId
2180
2212
  );
2181
2213
  const sessionInfos = await Promise.all(
2182
2214
  sessionHandles.map(
@@ -2191,21 +2223,21 @@ async function getLatestSessionInfo(userId) {
2191
2223
  }
2192
2224
  return latestSessionInfo;
2193
2225
  }
2194
- async function updateUserData(userId, inputData) {
2226
+ async function updateUserData(userId, inputData, tenantId = PUBLIC_TENANT_ID) {
2195
2227
  const metadata = await getUserMetadata(userId);
2196
2228
  const updatedMetadata = {
2197
2229
  ...metadata,
2198
2230
  ...inputData
2199
2231
  };
2200
2232
  await UserMetadata3.updateUserMetadata(userId, updatedMetadata);
2201
- return getUserById(userId);
2233
+ return getUserById(userId, tenantId);
2202
2234
  }
2203
2235
  async function startPendingEmailVerification(input) {
2204
2236
  const metadata = await getUserMetadata(input.userId);
2205
- const currentEmail = (await getUserById(input.userId)).data.email;
2237
+ const currentEmail = (await getUserById(input.userId, input.tenantId)).data.email;
2206
2238
  const pendingVerifications = getPendingVerifications(metadata);
2207
2239
  const pendingEmailVerifications = pendingVerifications.filter(
2208
- (pendingVerification2) => pendingVerification2.field === "email"
2240
+ (pendingVerification2) => pendingVerification2.field === "email" && (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) === input.tenantId
2209
2241
  );
2210
2242
  if (currentEmail === input.email) {
2211
2243
  for (const pendingVerification2 of pendingEmailVerifications) {
@@ -2220,11 +2252,11 @@ async function startPendingEmailVerification(input) {
2220
2252
  await UserMetadata3.updateUserMetadata(input.userId, {
2221
2253
  ...metadata,
2222
2254
  rownd_pending_verification: pendingVerifications.filter(
2223
- (pendingVerification2) => pendingVerification2.field !== "email"
2255
+ (pendingVerification2) => pendingVerification2.field !== "email" || (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) !== input.tenantId
2224
2256
  )
2225
2257
  });
2226
2258
  }
2227
- return getUserById(input.userId);
2259
+ return getUserById(input.userId, input.tenantId);
2228
2260
  }
2229
2261
  for (const pendingVerification2 of pendingEmailVerifications) {
2230
2262
  await EmailVerification.revokeEmailVerificationTokens(
@@ -2238,13 +2270,14 @@ async function startPendingEmailVerification(input) {
2238
2270
  id: input.pendingVerificationId,
2239
2271
  field: "email",
2240
2272
  value: input.email,
2241
- created_at: (/* @__PURE__ */ new Date()).toISOString()
2273
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2274
+ tenantId: input.tenantId
2242
2275
  };
2243
2276
  await UserMetadata3.updateUserMetadata(input.userId, {
2244
2277
  ...metadata,
2245
2278
  rownd_pending_verification: [
2246
2279
  ...pendingVerifications.filter(
2247
- (pendingVerification2) => pendingVerification2.field !== "email"
2280
+ (pendingVerification2) => pendingVerification2.field !== "email" || (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) !== input.tenantId
2248
2281
  ),
2249
2282
  pendingVerification
2250
2283
  ]
@@ -2263,12 +2296,14 @@ async function startPendingEmailVerification(input) {
2263
2296
  await completePendingEmailVerification({
2264
2297
  recipeUserId: input.recipeUserId,
2265
2298
  email: input.email,
2299
+ tenantId: input.tenantId,
2266
2300
  userContext: input.userContext
2267
2301
  });
2268
2302
  }
2269
- return getUserById(input.userId);
2303
+ return getUserById(input.userId, input.tenantId);
2270
2304
  }
2271
2305
  async function completePendingEmailVerification(input) {
2306
+ const tenantId = input.tenantId ?? PUBLIC_TENANT_ID;
2272
2307
  const user = await SuperTokens2.getUser(
2273
2308
  input.recipeUserId.getAsString(),
2274
2309
  input.userContext
@@ -2279,7 +2314,8 @@ async function completePendingEmailVerification(input) {
2279
2314
  const pendingVerification = pendingVerifications.find(
2280
2315
  (pendingVerification2) => isMatchingPendingEmailVerification(
2281
2316
  pendingVerification2,
2282
- input.email
2317
+ input.email,
2318
+ tenantId
2283
2319
  )
2284
2320
  );
2285
2321
  if (!pendingVerification) {
@@ -2287,7 +2323,12 @@ async function completePendingEmailVerification(input) {
2287
2323
  }
2288
2324
  let metadataUserId = userId;
2289
2325
  let verifiedRecipeUserId = input.recipeUserId;
2290
- const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(user);
2326
+ const tenantLoginMethods = user?.loginMethods.filter(
2327
+ (method) => method.tenantIds.includes(tenantId)
2328
+ ) ?? [];
2329
+ const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(
2330
+ tenantLoginMethods
2331
+ );
2291
2332
  if (passwordlessEmailMethod) {
2292
2333
  const updateResult = await Passwordless.updateUser({
2293
2334
  recipeUserId: passwordlessEmailMethod.recipeUserId,
@@ -2300,9 +2341,9 @@ async function completePendingEmailVerification(input) {
2300
2341
  );
2301
2342
  }
2302
2343
  verifiedRecipeUserId = passwordlessEmailMethod.recipeUserId;
2303
- } else if (hasOnlyGuestLoginMethods(user)) {
2344
+ } else if (hasOnlyGuestLoginMethods(user ? { ...user, loginMethods: tenantLoginMethods } : user)) {
2304
2345
  const isPasswordlessSignUpAllowed = await AccountLinking.isSignUpAllowed(
2305
- PUBLIC_TENANT_ID,
2346
+ tenantId,
2306
2347
  {
2307
2348
  recipeId: "passwordless",
2308
2349
  email: input.email
@@ -2316,7 +2357,7 @@ async function completePendingEmailVerification(input) {
2316
2357
  }
2317
2358
  const passwordlessUser = await Passwordless.signInUp({
2318
2359
  email: input.email,
2319
- tenantId: PUBLIC_TENANT_ID,
2360
+ tenantId,
2320
2361
  userContext: input.userContext
2321
2362
  });
2322
2363
  verifiedRecipeUserId = passwordlessUser.recipeUserId;
@@ -2360,7 +2401,8 @@ async function completePendingEmailVerification(input) {
2360
2401
  rownd_pending_verification: targetPendingVerifications.filter(
2361
2402
  (verification) => !isMatchingPendingEmailVerification(
2362
2403
  verification,
2363
- input.email
2404
+ input.email,
2405
+ tenantId
2364
2406
  )
2365
2407
  )
2366
2408
  };
@@ -2370,8 +2412,8 @@ async function completePendingEmailVerification(input) {
2370
2412
  recipeUserId: verifiedRecipeUserId
2371
2413
  };
2372
2414
  }
2373
- function isMatchingPendingEmailVerification(verification, email) {
2374
- return verification.field === "email" && verification.value === email;
2415
+ function isMatchingPendingEmailVerification(verification, email, tenantId) {
2416
+ return verification.field === "email" && verification.value === email && (verification.tenantId ?? PUBLIC_TENANT_ID) === tenantId;
2375
2417
  }
2376
2418
  async function updateUserMetadata(userId, inputMeta) {
2377
2419
  const metadata = await getUserMetadata(userId);
@@ -2389,8 +2431,8 @@ async function updateUserMetadata(userId, inputMeta) {
2389
2431
  )
2390
2432
  };
2391
2433
  }
2392
- function getPasswordlessEmailLoginMethod(user) {
2393
- return user?.loginMethods.find((method) => {
2434
+ function getPasswordlessEmailLoginMethod(loginMethods) {
2435
+ return loginMethods.find((method) => {
2394
2436
  return method.recipeId === "passwordless" && !!method.email;
2395
2437
  });
2396
2438
  }
@@ -2400,6 +2442,7 @@ import { randomUUID } from "crypto";
2400
2442
  import SuperTokens3 from "supertokens-node";
2401
2443
  import Session2 from "supertokens-node/recipe/session";
2402
2444
  import ThirdParty from "supertokens-node/recipe/thirdparty";
2445
+ import MultiTenancy from "supertokens-node/recipe/multitenancy";
2403
2446
  function isBodyString(body, key) {
2404
2447
  return isRecord(body) && typeof body[key] === "string" && body[key].length > 0;
2405
2448
  }
@@ -2464,7 +2507,9 @@ function handleGuestLogin(deps) {
2464
2507
  return async (req, res, _session, userContext) => {
2465
2508
  const startedAt = Date.now();
2466
2509
  const guestId = `guest_${randomUUID()}`;
2510
+ let tenantId;
2467
2511
  try {
2512
+ tenantId = resolveTenantId(req);
2468
2513
  const body = parseGuestBody(await getJsonBody(req));
2469
2514
  const appVariantId = getRequestedAppVariantIdFromRequest(req);
2470
2515
  assertRowndAppVariantIsConfigured(appVariantId);
@@ -2472,7 +2517,7 @@ function handleGuestLogin(deps) {
2472
2517
  const thirdPartyUserId = thirdPartyId === INSTANT_AUTH_METHOD_ID ? `anon_${randomUUID()}` : guestId;
2473
2518
  const authLevel = thirdPartyId === INSTANT_AUTH_METHOD_ID ? INSTANT_AUTH_METHOD_ID : GUEST_AUTH_METHOD_ID;
2474
2519
  const response = await ThirdParty.manuallyCreateOrUpdateUser(
2475
- PUBLIC_TENANT_ID,
2520
+ tenantId,
2476
2521
  thirdPartyId,
2477
2522
  thirdPartyUserId,
2478
2523
  `${thirdPartyUserId}@anonymous.local`,
@@ -2488,7 +2533,7 @@ function handleGuestLogin(deps) {
2488
2533
  await Session2.createNewSession(
2489
2534
  req,
2490
2535
  res,
2491
- PUBLIC_TENANT_ID,
2536
+ tenantId,
2492
2537
  response.recipeUserId,
2493
2538
  {
2494
2539
  ...buildRowndAudience({}, appVariantId),
@@ -2503,7 +2548,7 @@ function handleGuestLogin(deps) {
2503
2548
  deps.telemetryClient.recordSuccess({
2504
2549
  outcome: "success",
2505
2550
  durationMs: Date.now() - startedAt,
2506
- tenantId: PUBLIC_TENANT_ID,
2551
+ tenantId,
2507
2552
  superTokensUserId: response.user.id
2508
2553
  });
2509
2554
  return {
@@ -2515,7 +2560,7 @@ function handleGuestLogin(deps) {
2515
2560
  deps.telemetryClient.recordError({
2516
2561
  error,
2517
2562
  startedAt,
2518
- tenantId: PUBLIC_TENANT_ID
2563
+ tenantId
2519
2564
  });
2520
2565
  return {
2521
2566
  status: "ERROR",
@@ -2527,7 +2572,7 @@ function handleGuestLogin(deps) {
2527
2572
  function handleMigrate(deps) {
2528
2573
  return async (req, res, _session, userContext) => {
2529
2574
  const startedAt = Date.now();
2530
- let tenantId = PUBLIC_TENANT_ID;
2575
+ let tenantId;
2531
2576
  let rowndUserId;
2532
2577
  let superTokensUserId;
2533
2578
  let user;
@@ -2536,6 +2581,7 @@ function handleMigrate(deps) {
2536
2581
  if (!deps.stConfig.supertokens) {
2537
2582
  throw new Error("Supertokens config not found");
2538
2583
  }
2584
+ tenantId = resolveTenantId(req);
2539
2585
  const parsed = await parseRequest(req);
2540
2586
  const appVariantId = getRequestedAppVariantIdFromRequest(req);
2541
2587
  assertRowndAppVariantIsConfigured(appVariantId);
@@ -2543,13 +2589,16 @@ function handleMigrate(deps) {
2543
2589
  const rowndUser = await fetchOptionalRowndUserInfo(rowndUserId);
2544
2590
  if (!rowndUser) {
2545
2591
  logDebugMessage(
2546
- `Skipping migration because user does not exist in Rownd. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2592
+ `Skipping migration because user does not exist in Rownd. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2547
2593
  );
2548
2594
  return { status: "OK" };
2549
2595
  }
2550
2596
  user = await SuperTokens3.getUser(rowndUserId, userContext);
2551
2597
  if (!user) {
2552
- const stUserImport = mapRowndUserToSuperTokens(rowndUser);
2598
+ const stUserImport = mapRowndUserToSuperTokens(
2599
+ rowndUser,
2600
+ tenantId === PUBLIC_TENANT_ID ? void 0 : tenantId
2601
+ );
2553
2602
  try {
2554
2603
  await importUser(stUserImport, deps.stConfig.supertokens);
2555
2604
  clearSuperTokensCoreCallCache(userContext);
@@ -2567,29 +2616,43 @@ function handleMigrate(deps) {
2567
2616
  superTokensUserId = user.id;
2568
2617
  recipeUserId = user.loginMethods[0]?.recipeUserId;
2569
2618
  logDebugMessage(
2570
- `User already migrated (race condition). tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2619
+ `User already migrated (race condition). tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2571
2620
  );
2572
2621
  }
2573
2622
  logDebugMessage(
2574
- `User migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2623
+ `User migrated successfully. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2575
2624
  );
2576
2625
  } else {
2577
2626
  superTokensUserId = user.id;
2578
2627
  recipeUserId = user.loginMethods[0]?.recipeUserId;
2579
2628
  logDebugMessage(
2580
- `User already migrated. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2629
+ `User already migrated. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2581
2630
  );
2582
2631
  }
2583
2632
  if (superTokensUserId) {
2584
2633
  await recordRowndAppVariantForUser(superTokensUserId, appVariantId);
2585
2634
  }
2635
+ const tenantLoginMethod = user?.loginMethods.find(
2636
+ (method) => method.tenantIds.includes(tenantId)
2637
+ );
2638
+ recipeUserId = tenantLoginMethod?.recipeUserId ?? recipeUserId;
2586
2639
  if (!recipeUserId) {
2587
2640
  throw new Error("User not found or has no login methods");
2588
2641
  }
2642
+ if (!tenantLoginMethod) {
2643
+ const associationResult = await MultiTenancy.associateUserToTenant(
2644
+ tenantId,
2645
+ recipeUserId,
2646
+ userContext
2647
+ );
2648
+ if (associationResult.status !== "OK") {
2649
+ throw new Error(`Failed to associate migrated user with tenant: ${associationResult.status}`);
2650
+ }
2651
+ }
2589
2652
  await Session2.createNewSession(
2590
2653
  req,
2591
2654
  res,
2592
- PUBLIC_TENANT_ID,
2655
+ tenantId,
2593
2656
  recipeUserId,
2594
2657
  {
2595
2658
  ...buildRowndAudience({}, appVariantId)
@@ -2598,7 +2661,7 @@ function handleMigrate(deps) {
2598
2661
  userContext
2599
2662
  );
2600
2663
  logDebugMessage(
2601
- `Session migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, userId: ${superTokensUserId}`
2664
+ `Session migrated successfully. tenantId: ${tenantId}, userId: ${superTokensUserId}`
2602
2665
  );
2603
2666
  deps.telemetryClient.recordSuccess({
2604
2667
  outcome: "success",
@@ -2632,7 +2695,7 @@ function clearSuperTokensCoreCallCache(userContext) {
2632
2695
  }
2633
2696
  function handleGetUser() {
2634
2697
  return async (_req, _res, session) => {
2635
- const user = await getUserById(session.getUserId());
2698
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2636
2699
  return {
2637
2700
  status: "OK",
2638
2701
  ...user
@@ -2658,7 +2721,11 @@ function handleUpdateUser() {
2658
2721
  return permissionError;
2659
2722
  }
2660
2723
  if (Object.keys(dataWithoutEmail).length > 0) {
2661
- await updateUserData(session.getUserId(), dataWithoutEmail);
2724
+ await updateUserData(
2725
+ session.getUserId(),
2726
+ dataWithoutEmail,
2727
+ session.getTenantId()
2728
+ );
2662
2729
  }
2663
2730
  if (hasEmailUpdate) {
2664
2731
  const pendingVerificationResult = await startPendingEmailVerification({
@@ -2674,7 +2741,7 @@ function handleUpdateUser() {
2674
2741
  ...pendingVerificationResult
2675
2742
  };
2676
2743
  }
2677
- const user = await getUserById(session.getUserId());
2744
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2678
2745
  return {
2679
2746
  status: "OK",
2680
2747
  ...user
@@ -2741,7 +2808,7 @@ function handleGetUserField() {
2741
2808
  if (!field) {
2742
2809
  return missingFieldResponse();
2743
2810
  }
2744
- const user = await getUserById(session.getUserId());
2811
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2745
2812
  return {
2746
2813
  status: "OK",
2747
2814
  value: user.data[field]
@@ -2775,9 +2842,11 @@ function handleUpdateUserField() {
2775
2842
  if (permissionError) {
2776
2843
  return permissionError;
2777
2844
  }
2778
- const updateUserDataResult = await updateUserData(session.getUserId(), {
2779
- [field]: payload.value
2780
- });
2845
+ const updateUserDataResult = await updateUserData(
2846
+ session.getUserId(),
2847
+ { [field]: payload.value },
2848
+ session.getTenantId()
2849
+ );
2781
2850
  return {
2782
2851
  status: "OK",
2783
2852
  ...updateUserDataResult
@@ -2816,6 +2885,7 @@ var init = createPluginInitFunction(
2816
2885
  const displayContext = input?.userContext?.rowndDisplayContext;
2817
2886
  const redirectToPath = input?.userContext?.rowndRedirectToPath;
2818
2887
  const clientDomain = input?.userContext?.rowndClientDomain;
2888
+ const oauthLoginChallenge = input?.userContext?.rowndOAuthLoginChallenge;
2819
2889
  const clientDomainKey = clientDomain ?? (displayContext === "mobile_app" ? "mobile" : "browser");
2820
2890
  const clientBaseUrl = pluginConfig2.clientDomains?.[clientDomainKey];
2821
2891
  const bootstrapParams = {
@@ -2823,7 +2893,8 @@ var init = createPluginInitFunction(
2823
2893
  ...hubBootstrapParams ?? {},
2824
2894
  ...typeof appVariantId === "string" ? { appVariantId } : {},
2825
2895
  ...typeof displayContext === "string" ? { displayContext } : {},
2826
- ...typeof redirectToPath === "string" ? { redirectToPath } : {}
2896
+ ...typeof redirectToPath === "string" ? { redirectToPath } : {},
2897
+ ...typeof oauthLoginChallenge === "string" ? { oauthLoginChallenge } : {}
2827
2898
  };
2828
2899
  const rewrittenLink = input[linkKey] ? clientBaseUrl ? rewriteLinkToBaseUrl(
2829
2900
  input[linkKey],
@@ -3109,6 +3180,12 @@ var init = createPluginInitFunction(
3109
3180
  if (appVariantId) {
3110
3181
  input.userContext.rowndAppVariantId = appVariantId;
3111
3182
  }
3183
+ const oauthLoginChallenge = getRequestedOAuthLoginChallengeFromRequest(
3184
+ input.options.req
3185
+ );
3186
+ if (oauthLoginChallenge) {
3187
+ input.userContext.rowndOAuthLoginChallenge = oauthLoginChallenge;
3188
+ }
3112
3189
  assertRowndAppVariantIsConfigured(appVariantId);
3113
3190
  return originalImplementation.createCodePOST(input);
3114
3191
  },
@@ -3252,6 +3329,7 @@ var init = createPluginInitFunction(
3252
3329
  const verificationResult = await completePendingEmailVerification({
3253
3330
  recipeUserId: response.user.recipeUserId,
3254
3331
  email: response.user.email,
3332
+ tenantId: input.tenantId,
3255
3333
  userContext: input.userContext
3256
3334
  });
3257
3335
  const session = input.session;
@@ -49,10 +49,22 @@ rownd:
49
49
  # bearerToken: <ROWND_API_BEARER_TOKEN>
50
50
  # Number of Rownd users to request per page.
51
51
  pageSize: 100
52
+ # Rownd stores existing OIDC credential secrets hashed. Provide plaintext
53
+ # values here when migrating OIDC clients. Keys can be credential client IDs
54
+ # (key_...) and OIDC client configuration IDs (oc_...).
55
+ oidcClientSecrets: {}
56
+ # oidcClientSecrets:
57
+ # key_123: ras_plaintext_secret
58
+ # oc_123: ras_plaintext_secret_for_oidc_client_id_alias
59
+ # Rownd accepts the OIDC client configuration ID (oc_...) as a client_id in
60
+ # addition to each linked credential client_id (key_...).
61
+ provisionOidcClientIdAliases: true
52
62
 
53
63
  supertokens:
54
64
  connectionURI: <CONNECTION_URI>
55
65
  apiKey: <API_KEY>
66
+ # Tenant that receives every imported login method.
67
+ tenantId: public
56
68
  # Number of users to send to SuperTokens per bulk import request.
57
69
  batchSize: 500
58
70
  `;