@supertokens-plugins/rownd-nodejs 0.5.1 → 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/README.md CHANGED
@@ -116,6 +116,7 @@ import { createMagicLinkWithConfirmationBypass } from "@supertokens-plugins/rown
116
116
 
117
117
  const magicLink = await createMagicLinkWithConfirmationBypass({
118
118
  email: "user@example.com",
119
+ tenantId: "tenant-a",
119
120
  clientDomain: "browser",
120
121
  redirectToPath: "/profile",
121
122
  displayContext: "browser",
@@ -126,7 +127,7 @@ const magicLink = await createMagicLinkWithConfirmationBypass({
126
127
 
127
128
  `clientDomain` must be a configured `clientDomains` key, not a raw domain. Omit it to use the SuperTokens website domain.
128
129
 
129
- Pass exactly one of `email` or `phoneNumber`. The helper returns the rewritten magic link with `bypassDeviceConfirmation=true`.
130
+ Pass exactly one of `email` or `phoneNumber`. `tenantId` defaults to `public`. The helper returns the rewritten magic link with `bypassDeviceConfirmation=true`.
130
131
 
131
132
  Before skipping the cross-device confirmation prompt, the frontend should validate the callback against the plugin. Routes are mounted under your SuperTokens `apiBasePath`, which defaults to `/auth`.
132
133
 
@@ -141,16 +142,25 @@ If validation fails, the frontend should show the normal cross-device confirmati
141
142
 
142
143
  Routes are mounted under your SuperTokens `apiBasePath`, which defaults to `/auth`. The migration endpoint is the main Rownd-to-SuperTokens session handoff endpoint; the plugin also exposes Rownd-compatible app config, guest, user, metadata, field, and sign-out endpoints under `{apiBasePath}/plugin/rownd/...`.
143
144
 
145
+ Unauthenticated migration and guest routes accept an optional `tenantId` query parameter. It defaults to `public`. SuperTokens Core validates the tenant when the operation runs. Authenticated identity-field and sign-out operations use the tenant from the current session; custom Rownd metadata remains global to the SuperTokens user.
146
+
144
147
  > [!IMPORTANT]
145
- > The plugin always migrates users and sessions into the `public` tenant.
146
148
  > Rownd users with multiple supported login methods are rejected unless SuperTokens account linking is enabled in the target environment.
147
149
 
148
150
  ### Migrate
149
151
 
150
152
  - **POST** `{apiBasePath}/plugin/rownd/migrate`
151
153
  - **Default**: `POST /auth/plugin/rownd/migrate`
154
+ - **Non-public tenant**: `POST /auth/plugin/rownd/migrate?tenantId=tenant-a`
152
155
  - **Headers**: `Authorization: Bearer <Rownd_JWT>`. Header-token clients should also send `rid: session`, `fdi-version: 1.18`, and `st-auth-mode: header`.
153
- - **Description**: Validates the Rownd JWT, ensures the user is migrated to SuperTokens in the `public` tenant, syncs Rownd user data to SuperTokens UserMetadata, and then creates a new SuperTokens session for that user. Header-token clients must receive `st-access-token`, `st-refresh-token`, and `front-token` response headers.
156
+ - **Description**: Validates the Rownd JWT, imports new users with their Rownd profile data, ensures the selected login method is associated with the requested SuperTokens tenant, and then creates a new SuperTokens session in that tenant. Header-token clients must receive `st-access-token`, `st-refresh-token`, and `front-token` response headers.
157
+
158
+ ### Guest
159
+
160
+ - **POST** `{apiBasePath}/plugin/rownd/guest`
161
+ - **Default**: `POST /auth/plugin/rownd/guest`
162
+ - **Non-public tenant**: `POST /auth/plugin/rownd/guest?tenantId=tenant-a`
163
+ - **Description**: Creates a guest or instant user and session in the requested tenant.
154
164
 
155
165
  ## Debug Logging
156
166
 
@@ -228,6 +238,8 @@ RowndMigrationPlugin.init({
228
238
 
229
239
  The package includes a bulk migration script for importing Rownd users into SuperTokens.
230
240
 
241
+ Set `supertokens.tenantId` in the generated configuration to associate every imported login method with a non-public tenant. It defaults to `public` when omitted. Resuming from a checkpoint with a different tenant is rejected.
242
+
231
243
  The script runs from a YAML config file generated from the included template.
232
244
 
233
245
  ### Usage
@@ -64,7 +64,7 @@ function buildSuperTokensFakeEmail(thirdPartyUserId, thirdPartyId) {
64
64
  function isIdentityField(field) {
65
65
  return IDENTITY_USER_DATA_FIELDS.has(field);
66
66
  }
67
- function mapRowndUserToSuperTokens(rowndUser) {
67
+ function mapRowndUserToSuperTokens(rowndUser, tenantId) {
68
68
  const loginMethods = [];
69
69
  const rowndUserData = rowndUser.data || {};
70
70
  const rowndUserVerifiedData = rowndUser.verified_data || {};
@@ -78,7 +78,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
78
78
  thirdPartyId: "google",
79
79
  thirdPartyUserId: rowndUserData.google_id,
80
80
  email: googleEmail,
81
- isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id
81
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id,
82
+ ...tenantId ? { tenantIds: [tenantId] } : {}
82
83
  });
83
84
  }
84
85
  if (rowndUserData.apple_id) {
@@ -88,21 +89,24 @@ function mapRowndUserToSuperTokens(rowndUser) {
88
89
  thirdPartyId: "apple",
89
90
  thirdPartyUserId: rowndUserData.apple_id,
90
91
  email: appleEmail,
91
- isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id
92
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id,
93
+ ...tenantId ? { tenantIds: [tenantId] } : {}
92
94
  });
93
95
  }
94
96
  if (rowndUserData.phone_number) {
95
97
  loginMethods.push({
96
98
  recipeId: "passwordless",
97
99
  phoneNumber: rowndUserData.phone_number,
98
- isVerified: !!rowndUserVerifiedData.phone_number
100
+ isVerified: !!rowndUserVerifiedData.phone_number,
101
+ ...tenantId ? { tenantIds: [tenantId] } : {}
99
102
  });
100
103
  }
101
104
  if (rowndUserData.email && !rowndUserData.google_id && !rowndUserData.apple_id) {
102
105
  loginMethods.push({
103
106
  recipeId: "passwordless",
104
107
  email: rowndUserData.email,
105
- isVerified: !!rowndUserVerifiedData.email
108
+ isVerified: !!rowndUserVerifiedData.email,
109
+ ...tenantId ? { tenantIds: [tenantId] } : {}
106
110
  });
107
111
  }
108
112
  let authLevel = rowndUser.auth_level;
@@ -114,7 +118,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
114
118
  thirdPartyId,
115
119
  thirdPartyUserId: rowndUserData.user_id,
116
120
  email: `${rowndUserData.user_id}@anonymous.local`,
117
- isVerified: false
121
+ isVerified: false,
122
+ ...tenantId ? { tenantIds: [tenantId] } : {}
118
123
  });
119
124
  }
120
125
  const userMetadata = buildRowndUserMetadata(rowndUser);
@@ -168,7 +173,8 @@ var ConfigSchema = import_zod.z.object({
168
173
  supertokens: import_zod.z.object({
169
174
  connectionURI: import_zod.z.string(),
170
175
  apiKey: import_zod.z.string().optional(),
171
- batchSize: import_zod.z.number().int().positive()
176
+ batchSize: import_zod.z.number().int().positive(),
177
+ tenantId: import_zod.z.string().min(1).default("public")
172
178
  }),
173
179
  thirdPartyProviders: import_zod.z.array(
174
180
  import_zod.z.object({
@@ -366,6 +372,7 @@ Options:
366
372
  var CheckpointSchema = import_zod2.z.object({
367
373
  cursor: import_zod2.z.string().optional(),
368
374
  importedCount: import_zod2.z.number().int().nonnegative().optional(),
375
+ tenantId: import_zod2.z.string().optional(),
369
376
  updatedAt: import_zod2.z.string()
370
377
  });
371
378
  async function loadCheckpoint(checkpointFile) {
@@ -456,6 +463,11 @@ async function importUsersBatch(config, users, totalImportedBeforeBatch) {
456
463
  }
457
464
  async function migrateRowndUsersToSuperTokens(config) {
458
465
  const checkpoint = config.checkpoint.resume ? await loadCheckpoint(config.checkpoint.file) : null;
466
+ if (checkpoint && (checkpoint.tenantId ?? "public") !== config.supertokens.tenantId) {
467
+ throw new Error(
468
+ `Checkpoint tenant ${checkpoint.tenantId ?? "public"} does not match configured tenant ${config.supertokens.tenantId}`
469
+ );
470
+ }
459
471
  const failedMappingsFile = getFailedMappingsFilePath(config.checkpoint.file);
460
472
  let cursor = checkpoint?.cursor;
461
473
  let totalProcessed = 0;
@@ -489,7 +501,10 @@ async function migrateRowndUsersToSuperTokens(config) {
489
501
  try {
490
502
  mappedUsers.push({
491
503
  rowndUserId,
492
- user: mapRowndUserToSuperTokens(rowndUser)
504
+ user: mapRowndUserToSuperTokens(
505
+ rowndUser,
506
+ config.supertokens.tenantId === "public" ? void 0 : config.supertokens.tenantId
507
+ )
493
508
  });
494
509
  } catch (error) {
495
510
  const errorMessage = error instanceof Error ? error.message : "Unknown mapping error";
@@ -511,6 +526,7 @@ async function migrateRowndUsersToSuperTokens(config) {
511
526
  await saveCheckpoint(config.checkpoint.file, {
512
527
  cursor,
513
528
  importedCount: totalImported,
529
+ tenantId: config.supertokens.tenantId,
514
530
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
515
531
  });
516
532
  console.log(`Processed ${totalProcessed} users so far`);
@@ -68,7 +68,8 @@ var ConfigSchema = import_zod.z.object({
68
68
  supertokens: import_zod.z.object({
69
69
  connectionURI: import_zod.z.string(),
70
70
  apiKey: import_zod.z.string().optional(),
71
- batchSize: import_zod.z.number().int().positive()
71
+ batchSize: import_zod.z.number().int().positive(),
72
+ tenantId: import_zod.z.string().min(1).default("public")
72
73
  }),
73
74
  thirdPartyProviders: import_zod.z.array(
74
75
  import_zod.z.object({
package/dist/index.d.mts CHANGED
@@ -746,6 +746,7 @@ type RowndUserMetadata = {
746
746
  field: string;
747
747
  value: string;
748
748
  created_at: string;
749
+ tenantId?: string;
749
750
  }>;
750
751
  [key: string]: unknown;
751
752
  };
package/dist/index.d.ts CHANGED
@@ -746,6 +746,7 @@ type RowndUserMetadata = {
746
746
  field: string;
747
747
  value: string;
748
748
  created_at: string;
749
+ tenantId?: string;
749
750
  }>;
750
751
  [key: string]: unknown;
751
752
  };
package/dist/index.js CHANGED
@@ -866,6 +866,9 @@ var RowndPluginError = class extends Error {
866
866
 
867
867
  // src/utils.ts
868
868
  var ROWND_OAUTH_LOGIN_CHALLENGE_PARAM = "rownd_oauth_login_challenge";
869
+ function resolveTenantId(req) {
870
+ return req.getKeyValueFromQuery("tenantId") || PUBLIC_TENANT_ID;
871
+ }
869
872
  function rewriteLinkPath(inputUrl, targetPath, searchParams) {
870
873
  try {
871
874
  const url = new URL(inputUrl);
@@ -1497,7 +1500,7 @@ function isIdentityField(field) {
1497
1500
  function isInternalMetadataField(field) {
1498
1501
  return INTERNAL_METADATA_FIELDS.has(field);
1499
1502
  }
1500
- function mapRowndUserToSuperTokens(rowndUser) {
1503
+ function mapRowndUserToSuperTokens(rowndUser, tenantId) {
1501
1504
  const loginMethods = [];
1502
1505
  const rowndUserData = rowndUser.data || {};
1503
1506
  const rowndUserVerifiedData = rowndUser.verified_data || {};
@@ -1511,7 +1514,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
1511
1514
  thirdPartyId: "google",
1512
1515
  thirdPartyUserId: rowndUserData.google_id,
1513
1516
  email: googleEmail,
1514
- isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id
1517
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id,
1518
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1515
1519
  });
1516
1520
  }
1517
1521
  if (rowndUserData.apple_id) {
@@ -1521,21 +1525,24 @@ function mapRowndUserToSuperTokens(rowndUser) {
1521
1525
  thirdPartyId: "apple",
1522
1526
  thirdPartyUserId: rowndUserData.apple_id,
1523
1527
  email: appleEmail,
1524
- isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id
1528
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id,
1529
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1525
1530
  });
1526
1531
  }
1527
1532
  if (rowndUserData.phone_number) {
1528
1533
  loginMethods.push({
1529
1534
  recipeId: "passwordless",
1530
1535
  phoneNumber: rowndUserData.phone_number,
1531
- isVerified: !!rowndUserVerifiedData.phone_number
1536
+ isVerified: !!rowndUserVerifiedData.phone_number,
1537
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1532
1538
  });
1533
1539
  }
1534
1540
  if (rowndUserData.email && !rowndUserData.google_id && !rowndUserData.apple_id) {
1535
1541
  loginMethods.push({
1536
1542
  recipeId: "passwordless",
1537
1543
  email: rowndUserData.email,
1538
- isVerified: !!rowndUserVerifiedData.email
1544
+ isVerified: !!rowndUserVerifiedData.email,
1545
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1539
1546
  });
1540
1547
  }
1541
1548
  let authLevel = rowndUser.auth_level;
@@ -1547,7 +1554,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
1547
1554
  thirdPartyId,
1548
1555
  thirdPartyUserId: rowndUserData.user_id,
1549
1556
  email: `${rowndUserData.user_id}@anonymous.local`,
1550
- isVerified: false
1557
+ isVerified: false,
1558
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1551
1559
  });
1552
1560
  }
1553
1561
  const userMetadata = buildRowndUserMetadata(rowndUser);
@@ -2093,7 +2101,7 @@ function getPendingVerifications(metadata) {
2093
2101
  function isPendingVerification(value) {
2094
2102
  return isRecord(value) && typeof value.id === "string" && typeof value.field === "string" && typeof value.value === "string" && typeof value.created_at === "string";
2095
2103
  }
2096
- async function getUserById(userId) {
2104
+ async function getUserById(userId, tenantId = PUBLIC_TENANT_ID) {
2097
2105
  const metadata = await getUserMetadata(userId);
2098
2106
  const stUser = await import_supertokens_node2.default.getUser(userId);
2099
2107
  if (!stUser) {
@@ -2122,7 +2130,10 @@ async function getUserById(userId) {
2122
2130
  const verifiedData = {
2123
2131
  ...originalRowndUser?.verified_data || {}
2124
2132
  };
2125
- for (const method of stUser.loginMethods) {
2133
+ const tenantLoginMethods = stUser.loginMethods.filter(
2134
+ (method) => method.tenantIds.includes(tenantId)
2135
+ );
2136
+ for (const method of tenantLoginMethods) {
2126
2137
  if (method.recipeId === "passwordless") {
2127
2138
  if (method.email && !isSuperTokensFakeEmail(method.email)) {
2128
2139
  verifiedData.email = method.email;
@@ -2162,12 +2173,16 @@ async function getUserById(userId) {
2162
2173
  if (verifiedData.phone_number === true && typeof data.phone_number === "string") {
2163
2174
  verifiedData.phone_number = data.phone_number;
2164
2175
  }
2165
- const anonymousId = getAnonymousId(stUser.id, stUser, metadata);
2176
+ const tenantUser = {
2177
+ ...stUser,
2178
+ loginMethods: tenantLoginMethods
2179
+ };
2180
+ const anonymousId = getAnonymousId(stUser.id, tenantUser, metadata);
2166
2181
  if (anonymousId && data.anonymous_id === void 0) {
2167
2182
  data.anonymous_id = anonymousId;
2168
2183
  }
2169
2184
  const authLevel = getEffectiveAuthLevel(
2170
- stUser,
2185
+ tenantUser,
2171
2186
  originalRowndUser?.auth_level,
2172
2187
  verifiedData
2173
2188
  );
@@ -2176,15 +2191,15 @@ async function getUserById(userId) {
2176
2191
  data[key] = "";
2177
2192
  }
2178
2193
  }
2179
- const sortedByJoined = [...stUser.loginMethods].sort(
2194
+ const sortedByJoined = [...tenantLoginMethods].sort(
2180
2195
  (a, b) => a.timeJoined - b.timeJoined
2181
2196
  );
2182
- const latestSessionInfo = await getLatestSessionInfo(stUser.id);
2197
+ const latestSessionInfo = await getLatestSessionInfo(stUser.id, tenantId);
2183
2198
  const firstMethod = sortedByJoined[0];
2184
2199
  const latestSessionRecipeUserId = latestSessionInfo?.recipeUserId.getAsString();
2185
2200
  const lastMethod = latestSessionRecipeUserId ? stUser.loginMethods.find(
2186
2201
  (method) => method.recipeUserId.getAsString() === latestSessionRecipeUserId
2187
- ) : [...stUser.loginMethods].sort((a, b) => b.timeJoined - a.timeJoined)[0];
2202
+ ) : [...tenantLoginMethods].sort((a, b) => b.timeJoined - a.timeJoined)[0];
2188
2203
  const lastSignInAt = latestSessionInfo?.timeCreated ?? stUser.timeJoined;
2189
2204
  const metadataMeta = Object.fromEntries(
2190
2205
  Object.entries(metadata).filter(
@@ -2212,11 +2227,11 @@ async function getUserById(userId) {
2212
2227
  attributes: originalRowndUser?.attributes || {}
2213
2228
  };
2214
2229
  }
2215
- async function getLatestSessionInfo(userId) {
2230
+ async function getLatestSessionInfo(userId, tenantId) {
2216
2231
  const sessionHandles = await import_session.default.getAllSessionHandlesForUser(
2217
2232
  userId,
2218
2233
  true,
2219
- PUBLIC_TENANT_ID
2234
+ tenantId
2220
2235
  );
2221
2236
  const sessionInfos = await Promise.all(
2222
2237
  sessionHandles.map(
@@ -2231,21 +2246,21 @@ async function getLatestSessionInfo(userId) {
2231
2246
  }
2232
2247
  return latestSessionInfo;
2233
2248
  }
2234
- async function updateUserData(userId, inputData) {
2249
+ async function updateUserData(userId, inputData, tenantId = PUBLIC_TENANT_ID) {
2235
2250
  const metadata = await getUserMetadata(userId);
2236
2251
  const updatedMetadata = {
2237
2252
  ...metadata,
2238
2253
  ...inputData
2239
2254
  };
2240
2255
  await import_usermetadata3.default.updateUserMetadata(userId, updatedMetadata);
2241
- return getUserById(userId);
2256
+ return getUserById(userId, tenantId);
2242
2257
  }
2243
2258
  async function startPendingEmailVerification(input) {
2244
2259
  const metadata = await getUserMetadata(input.userId);
2245
- const currentEmail = (await getUserById(input.userId)).data.email;
2260
+ const currentEmail = (await getUserById(input.userId, input.tenantId)).data.email;
2246
2261
  const pendingVerifications = getPendingVerifications(metadata);
2247
2262
  const pendingEmailVerifications = pendingVerifications.filter(
2248
- (pendingVerification2) => pendingVerification2.field === "email"
2263
+ (pendingVerification2) => pendingVerification2.field === "email" && (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) === input.tenantId
2249
2264
  );
2250
2265
  if (currentEmail === input.email) {
2251
2266
  for (const pendingVerification2 of pendingEmailVerifications) {
@@ -2260,11 +2275,11 @@ async function startPendingEmailVerification(input) {
2260
2275
  await import_usermetadata3.default.updateUserMetadata(input.userId, {
2261
2276
  ...metadata,
2262
2277
  rownd_pending_verification: pendingVerifications.filter(
2263
- (pendingVerification2) => pendingVerification2.field !== "email"
2278
+ (pendingVerification2) => pendingVerification2.field !== "email" || (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) !== input.tenantId
2264
2279
  )
2265
2280
  });
2266
2281
  }
2267
- return getUserById(input.userId);
2282
+ return getUserById(input.userId, input.tenantId);
2268
2283
  }
2269
2284
  for (const pendingVerification2 of pendingEmailVerifications) {
2270
2285
  await import_emailverification.default.revokeEmailVerificationTokens(
@@ -2278,13 +2293,14 @@ async function startPendingEmailVerification(input) {
2278
2293
  id: input.pendingVerificationId,
2279
2294
  field: "email",
2280
2295
  value: input.email,
2281
- created_at: (/* @__PURE__ */ new Date()).toISOString()
2296
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2297
+ tenantId: input.tenantId
2282
2298
  };
2283
2299
  await import_usermetadata3.default.updateUserMetadata(input.userId, {
2284
2300
  ...metadata,
2285
2301
  rownd_pending_verification: [
2286
2302
  ...pendingVerifications.filter(
2287
- (pendingVerification2) => pendingVerification2.field !== "email"
2303
+ (pendingVerification2) => pendingVerification2.field !== "email" || (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) !== input.tenantId
2288
2304
  ),
2289
2305
  pendingVerification
2290
2306
  ]
@@ -2303,12 +2319,14 @@ async function startPendingEmailVerification(input) {
2303
2319
  await completePendingEmailVerification({
2304
2320
  recipeUserId: input.recipeUserId,
2305
2321
  email: input.email,
2322
+ tenantId: input.tenantId,
2306
2323
  userContext: input.userContext
2307
2324
  });
2308
2325
  }
2309
- return getUserById(input.userId);
2326
+ return getUserById(input.userId, input.tenantId);
2310
2327
  }
2311
2328
  async function completePendingEmailVerification(input) {
2329
+ const tenantId = input.tenantId ?? PUBLIC_TENANT_ID;
2312
2330
  const user = await import_supertokens_node2.default.getUser(
2313
2331
  input.recipeUserId.getAsString(),
2314
2332
  input.userContext
@@ -2319,7 +2337,8 @@ async function completePendingEmailVerification(input) {
2319
2337
  const pendingVerification = pendingVerifications.find(
2320
2338
  (pendingVerification2) => isMatchingPendingEmailVerification(
2321
2339
  pendingVerification2,
2322
- input.email
2340
+ input.email,
2341
+ tenantId
2323
2342
  )
2324
2343
  );
2325
2344
  if (!pendingVerification) {
@@ -2327,7 +2346,12 @@ async function completePendingEmailVerification(input) {
2327
2346
  }
2328
2347
  let metadataUserId = userId;
2329
2348
  let verifiedRecipeUserId = input.recipeUserId;
2330
- const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(user);
2349
+ const tenantLoginMethods = user?.loginMethods.filter(
2350
+ (method) => method.tenantIds.includes(tenantId)
2351
+ ) ?? [];
2352
+ const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(
2353
+ tenantLoginMethods
2354
+ );
2331
2355
  if (passwordlessEmailMethod) {
2332
2356
  const updateResult = await import_passwordless.default.updateUser({
2333
2357
  recipeUserId: passwordlessEmailMethod.recipeUserId,
@@ -2340,9 +2364,9 @@ async function completePendingEmailVerification(input) {
2340
2364
  );
2341
2365
  }
2342
2366
  verifiedRecipeUserId = passwordlessEmailMethod.recipeUserId;
2343
- } else if (hasOnlyGuestLoginMethods(user)) {
2367
+ } else if (hasOnlyGuestLoginMethods(user ? { ...user, loginMethods: tenantLoginMethods } : user)) {
2344
2368
  const isPasswordlessSignUpAllowed = await import_accountlinking.default.isSignUpAllowed(
2345
- PUBLIC_TENANT_ID,
2369
+ tenantId,
2346
2370
  {
2347
2371
  recipeId: "passwordless",
2348
2372
  email: input.email
@@ -2356,7 +2380,7 @@ async function completePendingEmailVerification(input) {
2356
2380
  }
2357
2381
  const passwordlessUser = await import_passwordless.default.signInUp({
2358
2382
  email: input.email,
2359
- tenantId: PUBLIC_TENANT_ID,
2383
+ tenantId,
2360
2384
  userContext: input.userContext
2361
2385
  });
2362
2386
  verifiedRecipeUserId = passwordlessUser.recipeUserId;
@@ -2400,7 +2424,8 @@ async function completePendingEmailVerification(input) {
2400
2424
  rownd_pending_verification: targetPendingVerifications.filter(
2401
2425
  (verification) => !isMatchingPendingEmailVerification(
2402
2426
  verification,
2403
- input.email
2427
+ input.email,
2428
+ tenantId
2404
2429
  )
2405
2430
  )
2406
2431
  };
@@ -2410,8 +2435,8 @@ async function completePendingEmailVerification(input) {
2410
2435
  recipeUserId: verifiedRecipeUserId
2411
2436
  };
2412
2437
  }
2413
- function isMatchingPendingEmailVerification(verification, email) {
2414
- return verification.field === "email" && verification.value === email;
2438
+ function isMatchingPendingEmailVerification(verification, email, tenantId) {
2439
+ return verification.field === "email" && verification.value === email && (verification.tenantId ?? PUBLIC_TENANT_ID) === tenantId;
2415
2440
  }
2416
2441
  async function updateUserMetadata(userId, inputMeta) {
2417
2442
  const metadata = await getUserMetadata(userId);
@@ -2429,8 +2454,8 @@ async function updateUserMetadata(userId, inputMeta) {
2429
2454
  )
2430
2455
  };
2431
2456
  }
2432
- function getPasswordlessEmailLoginMethod(user) {
2433
- return user?.loginMethods.find((method) => {
2457
+ function getPasswordlessEmailLoginMethod(loginMethods) {
2458
+ return loginMethods.find((method) => {
2434
2459
  return method.recipeId === "passwordless" && !!method.email;
2435
2460
  });
2436
2461
  }
@@ -2440,6 +2465,7 @@ var import_crypto = require("crypto");
2440
2465
  var import_supertokens_node3 = __toESM(require("supertokens-node"));
2441
2466
  var import_session2 = __toESM(require("supertokens-node/recipe/session"));
2442
2467
  var import_thirdparty = __toESM(require("supertokens-node/recipe/thirdparty"));
2468
+ var import_multitenancy = __toESM(require("supertokens-node/recipe/multitenancy"));
2443
2469
  function isBodyString(body, key) {
2444
2470
  return isRecord(body) && typeof body[key] === "string" && body[key].length > 0;
2445
2471
  }
@@ -2504,7 +2530,9 @@ function handleGuestLogin(deps) {
2504
2530
  return async (req, res, _session, userContext) => {
2505
2531
  const startedAt = Date.now();
2506
2532
  const guestId = `guest_${(0, import_crypto.randomUUID)()}`;
2533
+ let tenantId;
2507
2534
  try {
2535
+ tenantId = resolveTenantId(req);
2508
2536
  const body = parseGuestBody(await getJsonBody(req));
2509
2537
  const appVariantId = getRequestedAppVariantIdFromRequest(req);
2510
2538
  assertRowndAppVariantIsConfigured(appVariantId);
@@ -2512,7 +2540,7 @@ function handleGuestLogin(deps) {
2512
2540
  const thirdPartyUserId = thirdPartyId === INSTANT_AUTH_METHOD_ID ? `anon_${(0, import_crypto.randomUUID)()}` : guestId;
2513
2541
  const authLevel = thirdPartyId === INSTANT_AUTH_METHOD_ID ? INSTANT_AUTH_METHOD_ID : GUEST_AUTH_METHOD_ID;
2514
2542
  const response = await import_thirdparty.default.manuallyCreateOrUpdateUser(
2515
- PUBLIC_TENANT_ID,
2543
+ tenantId,
2516
2544
  thirdPartyId,
2517
2545
  thirdPartyUserId,
2518
2546
  `${thirdPartyUserId}@anonymous.local`,
@@ -2528,7 +2556,7 @@ function handleGuestLogin(deps) {
2528
2556
  await import_session2.default.createNewSession(
2529
2557
  req,
2530
2558
  res,
2531
- PUBLIC_TENANT_ID,
2559
+ tenantId,
2532
2560
  response.recipeUserId,
2533
2561
  {
2534
2562
  ...buildRowndAudience({}, appVariantId),
@@ -2543,7 +2571,7 @@ function handleGuestLogin(deps) {
2543
2571
  deps.telemetryClient.recordSuccess({
2544
2572
  outcome: "success",
2545
2573
  durationMs: Date.now() - startedAt,
2546
- tenantId: PUBLIC_TENANT_ID,
2574
+ tenantId,
2547
2575
  superTokensUserId: response.user.id
2548
2576
  });
2549
2577
  return {
@@ -2555,7 +2583,7 @@ function handleGuestLogin(deps) {
2555
2583
  deps.telemetryClient.recordError({
2556
2584
  error,
2557
2585
  startedAt,
2558
- tenantId: PUBLIC_TENANT_ID
2586
+ tenantId
2559
2587
  });
2560
2588
  return {
2561
2589
  status: "ERROR",
@@ -2567,7 +2595,7 @@ function handleGuestLogin(deps) {
2567
2595
  function handleMigrate(deps) {
2568
2596
  return async (req, res, _session, userContext) => {
2569
2597
  const startedAt = Date.now();
2570
- let tenantId = PUBLIC_TENANT_ID;
2598
+ let tenantId;
2571
2599
  let rowndUserId;
2572
2600
  let superTokensUserId;
2573
2601
  let user;
@@ -2576,6 +2604,7 @@ function handleMigrate(deps) {
2576
2604
  if (!deps.stConfig.supertokens) {
2577
2605
  throw new Error("Supertokens config not found");
2578
2606
  }
2607
+ tenantId = resolveTenantId(req);
2579
2608
  const parsed = await parseRequest(req);
2580
2609
  const appVariantId = getRequestedAppVariantIdFromRequest(req);
2581
2610
  assertRowndAppVariantIsConfigured(appVariantId);
@@ -2583,13 +2612,16 @@ function handleMigrate(deps) {
2583
2612
  const rowndUser = await fetchOptionalRowndUserInfo(rowndUserId);
2584
2613
  if (!rowndUser) {
2585
2614
  logDebugMessage(
2586
- `Skipping migration because user does not exist in Rownd. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2615
+ `Skipping migration because user does not exist in Rownd. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2587
2616
  );
2588
2617
  return { status: "OK" };
2589
2618
  }
2590
2619
  user = await import_supertokens_node3.default.getUser(rowndUserId, userContext);
2591
2620
  if (!user) {
2592
- const stUserImport = mapRowndUserToSuperTokens(rowndUser);
2621
+ const stUserImport = mapRowndUserToSuperTokens(
2622
+ rowndUser,
2623
+ tenantId === PUBLIC_TENANT_ID ? void 0 : tenantId
2624
+ );
2593
2625
  try {
2594
2626
  await importUser(stUserImport, deps.stConfig.supertokens);
2595
2627
  clearSuperTokensCoreCallCache(userContext);
@@ -2607,29 +2639,43 @@ function handleMigrate(deps) {
2607
2639
  superTokensUserId = user.id;
2608
2640
  recipeUserId = user.loginMethods[0]?.recipeUserId;
2609
2641
  logDebugMessage(
2610
- `User already migrated (race condition). tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2642
+ `User already migrated (race condition). tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2611
2643
  );
2612
2644
  }
2613
2645
  logDebugMessage(
2614
- `User migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2646
+ `User migrated successfully. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2615
2647
  );
2616
2648
  } else {
2617
2649
  superTokensUserId = user.id;
2618
2650
  recipeUserId = user.loginMethods[0]?.recipeUserId;
2619
2651
  logDebugMessage(
2620
- `User already migrated. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2652
+ `User already migrated. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2621
2653
  );
2622
2654
  }
2623
2655
  if (superTokensUserId) {
2624
2656
  await recordRowndAppVariantForUser(superTokensUserId, appVariantId);
2625
2657
  }
2658
+ const tenantLoginMethod = user?.loginMethods.find(
2659
+ (method) => method.tenantIds.includes(tenantId)
2660
+ );
2661
+ recipeUserId = tenantLoginMethod?.recipeUserId ?? recipeUserId;
2626
2662
  if (!recipeUserId) {
2627
2663
  throw new Error("User not found or has no login methods");
2628
2664
  }
2665
+ if (!tenantLoginMethod) {
2666
+ const associationResult = await import_multitenancy.default.associateUserToTenant(
2667
+ tenantId,
2668
+ recipeUserId,
2669
+ userContext
2670
+ );
2671
+ if (associationResult.status !== "OK") {
2672
+ throw new Error(`Failed to associate migrated user with tenant: ${associationResult.status}`);
2673
+ }
2674
+ }
2629
2675
  await import_session2.default.createNewSession(
2630
2676
  req,
2631
2677
  res,
2632
- PUBLIC_TENANT_ID,
2678
+ tenantId,
2633
2679
  recipeUserId,
2634
2680
  {
2635
2681
  ...buildRowndAudience({}, appVariantId)
@@ -2638,7 +2684,7 @@ function handleMigrate(deps) {
2638
2684
  userContext
2639
2685
  );
2640
2686
  logDebugMessage(
2641
- `Session migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, userId: ${superTokensUserId}`
2687
+ `Session migrated successfully. tenantId: ${tenantId}, userId: ${superTokensUserId}`
2642
2688
  );
2643
2689
  deps.telemetryClient.recordSuccess({
2644
2690
  outcome: "success",
@@ -2672,7 +2718,7 @@ function clearSuperTokensCoreCallCache(userContext) {
2672
2718
  }
2673
2719
  function handleGetUser() {
2674
2720
  return async (_req, _res, session) => {
2675
- const user = await getUserById(session.getUserId());
2721
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2676
2722
  return {
2677
2723
  status: "OK",
2678
2724
  ...user
@@ -2698,7 +2744,11 @@ function handleUpdateUser() {
2698
2744
  return permissionError;
2699
2745
  }
2700
2746
  if (Object.keys(dataWithoutEmail).length > 0) {
2701
- await updateUserData(session.getUserId(), dataWithoutEmail);
2747
+ await updateUserData(
2748
+ session.getUserId(),
2749
+ dataWithoutEmail,
2750
+ session.getTenantId()
2751
+ );
2702
2752
  }
2703
2753
  if (hasEmailUpdate) {
2704
2754
  const pendingVerificationResult = await startPendingEmailVerification({
@@ -2714,7 +2764,7 @@ function handleUpdateUser() {
2714
2764
  ...pendingVerificationResult
2715
2765
  };
2716
2766
  }
2717
- const user = await getUserById(session.getUserId());
2767
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2718
2768
  return {
2719
2769
  status: "OK",
2720
2770
  ...user
@@ -2781,7 +2831,7 @@ function handleGetUserField() {
2781
2831
  if (!field) {
2782
2832
  return missingFieldResponse();
2783
2833
  }
2784
- const user = await getUserById(session.getUserId());
2834
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2785
2835
  return {
2786
2836
  status: "OK",
2787
2837
  value: user.data[field]
@@ -2815,9 +2865,11 @@ function handleUpdateUserField() {
2815
2865
  if (permissionError) {
2816
2866
  return permissionError;
2817
2867
  }
2818
- const updateUserDataResult = await updateUserData(session.getUserId(), {
2819
- [field]: payload.value
2820
- });
2868
+ const updateUserDataResult = await updateUserData(
2869
+ session.getUserId(),
2870
+ { [field]: payload.value },
2871
+ session.getTenantId()
2872
+ );
2821
2873
  return {
2822
2874
  status: "OK",
2823
2875
  ...updateUserDataResult
@@ -3300,6 +3352,7 @@ var init = createPluginInitFunction(
3300
3352
  const verificationResult = await completePendingEmailVerification({
3301
3353
  recipeUserId: response.user.recipeUserId,
3302
3354
  email: response.user.email,
3355
+ tenantId: input.tenantId,
3303
3356
  userContext: input.userContext
3304
3357
  });
3305
3358
  const session = input.session;
package/dist/index.mjs CHANGED
@@ -843,6 +843,9 @@ var RowndPluginError = class extends Error {
843
843
 
844
844
  // src/utils.ts
845
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);
@@ -1474,7 +1477,7 @@ function isIdentityField(field) {
1474
1477
  function isInternalMetadataField(field) {
1475
1478
  return INTERNAL_METADATA_FIELDS.has(field);
1476
1479
  }
1477
- function mapRowndUserToSuperTokens(rowndUser) {
1480
+ function mapRowndUserToSuperTokens(rowndUser, tenantId) {
1478
1481
  const loginMethods = [];
1479
1482
  const rowndUserData = rowndUser.data || {};
1480
1483
  const rowndUserVerifiedData = rowndUser.verified_data || {};
@@ -1488,7 +1491,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
1488
1491
  thirdPartyId: "google",
1489
1492
  thirdPartyUserId: rowndUserData.google_id,
1490
1493
  email: googleEmail,
1491
- isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id
1494
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id,
1495
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1492
1496
  });
1493
1497
  }
1494
1498
  if (rowndUserData.apple_id) {
@@ -1498,21 +1502,24 @@ function mapRowndUserToSuperTokens(rowndUser) {
1498
1502
  thirdPartyId: "apple",
1499
1503
  thirdPartyUserId: rowndUserData.apple_id,
1500
1504
  email: appleEmail,
1501
- isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id
1505
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id,
1506
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1502
1507
  });
1503
1508
  }
1504
1509
  if (rowndUserData.phone_number) {
1505
1510
  loginMethods.push({
1506
1511
  recipeId: "passwordless",
1507
1512
  phoneNumber: rowndUserData.phone_number,
1508
- isVerified: !!rowndUserVerifiedData.phone_number
1513
+ isVerified: !!rowndUserVerifiedData.phone_number,
1514
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1509
1515
  });
1510
1516
  }
1511
1517
  if (rowndUserData.email && !rowndUserData.google_id && !rowndUserData.apple_id) {
1512
1518
  loginMethods.push({
1513
1519
  recipeId: "passwordless",
1514
1520
  email: rowndUserData.email,
1515
- isVerified: !!rowndUserVerifiedData.email
1521
+ isVerified: !!rowndUserVerifiedData.email,
1522
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1516
1523
  });
1517
1524
  }
1518
1525
  let authLevel = rowndUser.auth_level;
@@ -1524,7 +1531,8 @@ function mapRowndUserToSuperTokens(rowndUser) {
1524
1531
  thirdPartyId,
1525
1532
  thirdPartyUserId: rowndUserData.user_id,
1526
1533
  email: `${rowndUserData.user_id}@anonymous.local`,
1527
- isVerified: false
1534
+ isVerified: false,
1535
+ ...tenantId ? { tenantIds: [tenantId] } : {}
1528
1536
  });
1529
1537
  }
1530
1538
  const userMetadata = buildRowndUserMetadata(rowndUser);
@@ -2070,7 +2078,7 @@ function getPendingVerifications(metadata) {
2070
2078
  function isPendingVerification(value) {
2071
2079
  return isRecord(value) && typeof value.id === "string" && typeof value.field === "string" && typeof value.value === "string" && typeof value.created_at === "string";
2072
2080
  }
2073
- async function getUserById(userId) {
2081
+ async function getUserById(userId, tenantId = PUBLIC_TENANT_ID) {
2074
2082
  const metadata = await getUserMetadata(userId);
2075
2083
  const stUser = await SuperTokens2.getUser(userId);
2076
2084
  if (!stUser) {
@@ -2099,7 +2107,10 @@ async function getUserById(userId) {
2099
2107
  const verifiedData = {
2100
2108
  ...originalRowndUser?.verified_data || {}
2101
2109
  };
2102
- 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) {
2103
2114
  if (method.recipeId === "passwordless") {
2104
2115
  if (method.email && !isSuperTokensFakeEmail(method.email)) {
2105
2116
  verifiedData.email = method.email;
@@ -2139,12 +2150,16 @@ async function getUserById(userId) {
2139
2150
  if (verifiedData.phone_number === true && typeof data.phone_number === "string") {
2140
2151
  verifiedData.phone_number = data.phone_number;
2141
2152
  }
2142
- const anonymousId = getAnonymousId(stUser.id, stUser, metadata);
2153
+ const tenantUser = {
2154
+ ...stUser,
2155
+ loginMethods: tenantLoginMethods
2156
+ };
2157
+ const anonymousId = getAnonymousId(stUser.id, tenantUser, metadata);
2143
2158
  if (anonymousId && data.anonymous_id === void 0) {
2144
2159
  data.anonymous_id = anonymousId;
2145
2160
  }
2146
2161
  const authLevel = getEffectiveAuthLevel(
2147
- stUser,
2162
+ tenantUser,
2148
2163
  originalRowndUser?.auth_level,
2149
2164
  verifiedData
2150
2165
  );
@@ -2153,15 +2168,15 @@ async function getUserById(userId) {
2153
2168
  data[key] = "";
2154
2169
  }
2155
2170
  }
2156
- const sortedByJoined = [...stUser.loginMethods].sort(
2171
+ const sortedByJoined = [...tenantLoginMethods].sort(
2157
2172
  (a, b) => a.timeJoined - b.timeJoined
2158
2173
  );
2159
- const latestSessionInfo = await getLatestSessionInfo(stUser.id);
2174
+ const latestSessionInfo = await getLatestSessionInfo(stUser.id, tenantId);
2160
2175
  const firstMethod = sortedByJoined[0];
2161
2176
  const latestSessionRecipeUserId = latestSessionInfo?.recipeUserId.getAsString();
2162
2177
  const lastMethod = latestSessionRecipeUserId ? stUser.loginMethods.find(
2163
2178
  (method) => method.recipeUserId.getAsString() === latestSessionRecipeUserId
2164
- ) : [...stUser.loginMethods].sort((a, b) => b.timeJoined - a.timeJoined)[0];
2179
+ ) : [...tenantLoginMethods].sort((a, b) => b.timeJoined - a.timeJoined)[0];
2165
2180
  const lastSignInAt = latestSessionInfo?.timeCreated ?? stUser.timeJoined;
2166
2181
  const metadataMeta = Object.fromEntries(
2167
2182
  Object.entries(metadata).filter(
@@ -2189,11 +2204,11 @@ async function getUserById(userId) {
2189
2204
  attributes: originalRowndUser?.attributes || {}
2190
2205
  };
2191
2206
  }
2192
- async function getLatestSessionInfo(userId) {
2207
+ async function getLatestSessionInfo(userId, tenantId) {
2193
2208
  const sessionHandles = await Session.getAllSessionHandlesForUser(
2194
2209
  userId,
2195
2210
  true,
2196
- PUBLIC_TENANT_ID
2211
+ tenantId
2197
2212
  );
2198
2213
  const sessionInfos = await Promise.all(
2199
2214
  sessionHandles.map(
@@ -2208,21 +2223,21 @@ async function getLatestSessionInfo(userId) {
2208
2223
  }
2209
2224
  return latestSessionInfo;
2210
2225
  }
2211
- async function updateUserData(userId, inputData) {
2226
+ async function updateUserData(userId, inputData, tenantId = PUBLIC_TENANT_ID) {
2212
2227
  const metadata = await getUserMetadata(userId);
2213
2228
  const updatedMetadata = {
2214
2229
  ...metadata,
2215
2230
  ...inputData
2216
2231
  };
2217
2232
  await UserMetadata3.updateUserMetadata(userId, updatedMetadata);
2218
- return getUserById(userId);
2233
+ return getUserById(userId, tenantId);
2219
2234
  }
2220
2235
  async function startPendingEmailVerification(input) {
2221
2236
  const metadata = await getUserMetadata(input.userId);
2222
- const currentEmail = (await getUserById(input.userId)).data.email;
2237
+ const currentEmail = (await getUserById(input.userId, input.tenantId)).data.email;
2223
2238
  const pendingVerifications = getPendingVerifications(metadata);
2224
2239
  const pendingEmailVerifications = pendingVerifications.filter(
2225
- (pendingVerification2) => pendingVerification2.field === "email"
2240
+ (pendingVerification2) => pendingVerification2.field === "email" && (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) === input.tenantId
2226
2241
  );
2227
2242
  if (currentEmail === input.email) {
2228
2243
  for (const pendingVerification2 of pendingEmailVerifications) {
@@ -2237,11 +2252,11 @@ async function startPendingEmailVerification(input) {
2237
2252
  await UserMetadata3.updateUserMetadata(input.userId, {
2238
2253
  ...metadata,
2239
2254
  rownd_pending_verification: pendingVerifications.filter(
2240
- (pendingVerification2) => pendingVerification2.field !== "email"
2255
+ (pendingVerification2) => pendingVerification2.field !== "email" || (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) !== input.tenantId
2241
2256
  )
2242
2257
  });
2243
2258
  }
2244
- return getUserById(input.userId);
2259
+ return getUserById(input.userId, input.tenantId);
2245
2260
  }
2246
2261
  for (const pendingVerification2 of pendingEmailVerifications) {
2247
2262
  await EmailVerification.revokeEmailVerificationTokens(
@@ -2255,13 +2270,14 @@ async function startPendingEmailVerification(input) {
2255
2270
  id: input.pendingVerificationId,
2256
2271
  field: "email",
2257
2272
  value: input.email,
2258
- created_at: (/* @__PURE__ */ new Date()).toISOString()
2273
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2274
+ tenantId: input.tenantId
2259
2275
  };
2260
2276
  await UserMetadata3.updateUserMetadata(input.userId, {
2261
2277
  ...metadata,
2262
2278
  rownd_pending_verification: [
2263
2279
  ...pendingVerifications.filter(
2264
- (pendingVerification2) => pendingVerification2.field !== "email"
2280
+ (pendingVerification2) => pendingVerification2.field !== "email" || (pendingVerification2.tenantId ?? PUBLIC_TENANT_ID) !== input.tenantId
2265
2281
  ),
2266
2282
  pendingVerification
2267
2283
  ]
@@ -2280,12 +2296,14 @@ async function startPendingEmailVerification(input) {
2280
2296
  await completePendingEmailVerification({
2281
2297
  recipeUserId: input.recipeUserId,
2282
2298
  email: input.email,
2299
+ tenantId: input.tenantId,
2283
2300
  userContext: input.userContext
2284
2301
  });
2285
2302
  }
2286
- return getUserById(input.userId);
2303
+ return getUserById(input.userId, input.tenantId);
2287
2304
  }
2288
2305
  async function completePendingEmailVerification(input) {
2306
+ const tenantId = input.tenantId ?? PUBLIC_TENANT_ID;
2289
2307
  const user = await SuperTokens2.getUser(
2290
2308
  input.recipeUserId.getAsString(),
2291
2309
  input.userContext
@@ -2296,7 +2314,8 @@ async function completePendingEmailVerification(input) {
2296
2314
  const pendingVerification = pendingVerifications.find(
2297
2315
  (pendingVerification2) => isMatchingPendingEmailVerification(
2298
2316
  pendingVerification2,
2299
- input.email
2317
+ input.email,
2318
+ tenantId
2300
2319
  )
2301
2320
  );
2302
2321
  if (!pendingVerification) {
@@ -2304,7 +2323,12 @@ async function completePendingEmailVerification(input) {
2304
2323
  }
2305
2324
  let metadataUserId = userId;
2306
2325
  let verifiedRecipeUserId = input.recipeUserId;
2307
- const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(user);
2326
+ const tenantLoginMethods = user?.loginMethods.filter(
2327
+ (method) => method.tenantIds.includes(tenantId)
2328
+ ) ?? [];
2329
+ const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(
2330
+ tenantLoginMethods
2331
+ );
2308
2332
  if (passwordlessEmailMethod) {
2309
2333
  const updateResult = await Passwordless.updateUser({
2310
2334
  recipeUserId: passwordlessEmailMethod.recipeUserId,
@@ -2317,9 +2341,9 @@ async function completePendingEmailVerification(input) {
2317
2341
  );
2318
2342
  }
2319
2343
  verifiedRecipeUserId = passwordlessEmailMethod.recipeUserId;
2320
- } else if (hasOnlyGuestLoginMethods(user)) {
2344
+ } else if (hasOnlyGuestLoginMethods(user ? { ...user, loginMethods: tenantLoginMethods } : user)) {
2321
2345
  const isPasswordlessSignUpAllowed = await AccountLinking.isSignUpAllowed(
2322
- PUBLIC_TENANT_ID,
2346
+ tenantId,
2323
2347
  {
2324
2348
  recipeId: "passwordless",
2325
2349
  email: input.email
@@ -2333,7 +2357,7 @@ async function completePendingEmailVerification(input) {
2333
2357
  }
2334
2358
  const passwordlessUser = await Passwordless.signInUp({
2335
2359
  email: input.email,
2336
- tenantId: PUBLIC_TENANT_ID,
2360
+ tenantId,
2337
2361
  userContext: input.userContext
2338
2362
  });
2339
2363
  verifiedRecipeUserId = passwordlessUser.recipeUserId;
@@ -2377,7 +2401,8 @@ async function completePendingEmailVerification(input) {
2377
2401
  rownd_pending_verification: targetPendingVerifications.filter(
2378
2402
  (verification) => !isMatchingPendingEmailVerification(
2379
2403
  verification,
2380
- input.email
2404
+ input.email,
2405
+ tenantId
2381
2406
  )
2382
2407
  )
2383
2408
  };
@@ -2387,8 +2412,8 @@ async function completePendingEmailVerification(input) {
2387
2412
  recipeUserId: verifiedRecipeUserId
2388
2413
  };
2389
2414
  }
2390
- function isMatchingPendingEmailVerification(verification, email) {
2391
- 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;
2392
2417
  }
2393
2418
  async function updateUserMetadata(userId, inputMeta) {
2394
2419
  const metadata = await getUserMetadata(userId);
@@ -2406,8 +2431,8 @@ async function updateUserMetadata(userId, inputMeta) {
2406
2431
  )
2407
2432
  };
2408
2433
  }
2409
- function getPasswordlessEmailLoginMethod(user) {
2410
- return user?.loginMethods.find((method) => {
2434
+ function getPasswordlessEmailLoginMethod(loginMethods) {
2435
+ return loginMethods.find((method) => {
2411
2436
  return method.recipeId === "passwordless" && !!method.email;
2412
2437
  });
2413
2438
  }
@@ -2417,6 +2442,7 @@ import { randomUUID } from "crypto";
2417
2442
  import SuperTokens3 from "supertokens-node";
2418
2443
  import Session2 from "supertokens-node/recipe/session";
2419
2444
  import ThirdParty from "supertokens-node/recipe/thirdparty";
2445
+ import MultiTenancy from "supertokens-node/recipe/multitenancy";
2420
2446
  function isBodyString(body, key) {
2421
2447
  return isRecord(body) && typeof body[key] === "string" && body[key].length > 0;
2422
2448
  }
@@ -2481,7 +2507,9 @@ function handleGuestLogin(deps) {
2481
2507
  return async (req, res, _session, userContext) => {
2482
2508
  const startedAt = Date.now();
2483
2509
  const guestId = `guest_${randomUUID()}`;
2510
+ let tenantId;
2484
2511
  try {
2512
+ tenantId = resolveTenantId(req);
2485
2513
  const body = parseGuestBody(await getJsonBody(req));
2486
2514
  const appVariantId = getRequestedAppVariantIdFromRequest(req);
2487
2515
  assertRowndAppVariantIsConfigured(appVariantId);
@@ -2489,7 +2517,7 @@ function handleGuestLogin(deps) {
2489
2517
  const thirdPartyUserId = thirdPartyId === INSTANT_AUTH_METHOD_ID ? `anon_${randomUUID()}` : guestId;
2490
2518
  const authLevel = thirdPartyId === INSTANT_AUTH_METHOD_ID ? INSTANT_AUTH_METHOD_ID : GUEST_AUTH_METHOD_ID;
2491
2519
  const response = await ThirdParty.manuallyCreateOrUpdateUser(
2492
- PUBLIC_TENANT_ID,
2520
+ tenantId,
2493
2521
  thirdPartyId,
2494
2522
  thirdPartyUserId,
2495
2523
  `${thirdPartyUserId}@anonymous.local`,
@@ -2505,7 +2533,7 @@ function handleGuestLogin(deps) {
2505
2533
  await Session2.createNewSession(
2506
2534
  req,
2507
2535
  res,
2508
- PUBLIC_TENANT_ID,
2536
+ tenantId,
2509
2537
  response.recipeUserId,
2510
2538
  {
2511
2539
  ...buildRowndAudience({}, appVariantId),
@@ -2520,7 +2548,7 @@ function handleGuestLogin(deps) {
2520
2548
  deps.telemetryClient.recordSuccess({
2521
2549
  outcome: "success",
2522
2550
  durationMs: Date.now() - startedAt,
2523
- tenantId: PUBLIC_TENANT_ID,
2551
+ tenantId,
2524
2552
  superTokensUserId: response.user.id
2525
2553
  });
2526
2554
  return {
@@ -2532,7 +2560,7 @@ function handleGuestLogin(deps) {
2532
2560
  deps.telemetryClient.recordError({
2533
2561
  error,
2534
2562
  startedAt,
2535
- tenantId: PUBLIC_TENANT_ID
2563
+ tenantId
2536
2564
  });
2537
2565
  return {
2538
2566
  status: "ERROR",
@@ -2544,7 +2572,7 @@ function handleGuestLogin(deps) {
2544
2572
  function handleMigrate(deps) {
2545
2573
  return async (req, res, _session, userContext) => {
2546
2574
  const startedAt = Date.now();
2547
- let tenantId = PUBLIC_TENANT_ID;
2575
+ let tenantId;
2548
2576
  let rowndUserId;
2549
2577
  let superTokensUserId;
2550
2578
  let user;
@@ -2553,6 +2581,7 @@ function handleMigrate(deps) {
2553
2581
  if (!deps.stConfig.supertokens) {
2554
2582
  throw new Error("Supertokens config not found");
2555
2583
  }
2584
+ tenantId = resolveTenantId(req);
2556
2585
  const parsed = await parseRequest(req);
2557
2586
  const appVariantId = getRequestedAppVariantIdFromRequest(req);
2558
2587
  assertRowndAppVariantIsConfigured(appVariantId);
@@ -2560,13 +2589,16 @@ function handleMigrate(deps) {
2560
2589
  const rowndUser = await fetchOptionalRowndUserInfo(rowndUserId);
2561
2590
  if (!rowndUser) {
2562
2591
  logDebugMessage(
2563
- `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}`
2564
2593
  );
2565
2594
  return { status: "OK" };
2566
2595
  }
2567
2596
  user = await SuperTokens3.getUser(rowndUserId, userContext);
2568
2597
  if (!user) {
2569
- const stUserImport = mapRowndUserToSuperTokens(rowndUser);
2598
+ const stUserImport = mapRowndUserToSuperTokens(
2599
+ rowndUser,
2600
+ tenantId === PUBLIC_TENANT_ID ? void 0 : tenantId
2601
+ );
2570
2602
  try {
2571
2603
  await importUser(stUserImport, deps.stConfig.supertokens);
2572
2604
  clearSuperTokensCoreCallCache(userContext);
@@ -2584,29 +2616,43 @@ function handleMigrate(deps) {
2584
2616
  superTokensUserId = user.id;
2585
2617
  recipeUserId = user.loginMethods[0]?.recipeUserId;
2586
2618
  logDebugMessage(
2587
- `User already migrated (race condition). tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2619
+ `User already migrated (race condition). tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2588
2620
  );
2589
2621
  }
2590
2622
  logDebugMessage(
2591
- `User migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2623
+ `User migrated successfully. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2592
2624
  );
2593
2625
  } else {
2594
2626
  superTokensUserId = user.id;
2595
2627
  recipeUserId = user.loginMethods[0]?.recipeUserId;
2596
2628
  logDebugMessage(
2597
- `User already migrated. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`
2629
+ `User already migrated. tenantId: ${tenantId}, rowndUserId: ${rowndUserId}`
2598
2630
  );
2599
2631
  }
2600
2632
  if (superTokensUserId) {
2601
2633
  await recordRowndAppVariantForUser(superTokensUserId, appVariantId);
2602
2634
  }
2635
+ const tenantLoginMethod = user?.loginMethods.find(
2636
+ (method) => method.tenantIds.includes(tenantId)
2637
+ );
2638
+ recipeUserId = tenantLoginMethod?.recipeUserId ?? recipeUserId;
2603
2639
  if (!recipeUserId) {
2604
2640
  throw new Error("User not found or has no login methods");
2605
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
+ }
2606
2652
  await Session2.createNewSession(
2607
2653
  req,
2608
2654
  res,
2609
- PUBLIC_TENANT_ID,
2655
+ tenantId,
2610
2656
  recipeUserId,
2611
2657
  {
2612
2658
  ...buildRowndAudience({}, appVariantId)
@@ -2615,7 +2661,7 @@ function handleMigrate(deps) {
2615
2661
  userContext
2616
2662
  );
2617
2663
  logDebugMessage(
2618
- `Session migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, userId: ${superTokensUserId}`
2664
+ `Session migrated successfully. tenantId: ${tenantId}, userId: ${superTokensUserId}`
2619
2665
  );
2620
2666
  deps.telemetryClient.recordSuccess({
2621
2667
  outcome: "success",
@@ -2649,7 +2695,7 @@ function clearSuperTokensCoreCallCache(userContext) {
2649
2695
  }
2650
2696
  function handleGetUser() {
2651
2697
  return async (_req, _res, session) => {
2652
- const user = await getUserById(session.getUserId());
2698
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2653
2699
  return {
2654
2700
  status: "OK",
2655
2701
  ...user
@@ -2675,7 +2721,11 @@ function handleUpdateUser() {
2675
2721
  return permissionError;
2676
2722
  }
2677
2723
  if (Object.keys(dataWithoutEmail).length > 0) {
2678
- await updateUserData(session.getUserId(), dataWithoutEmail);
2724
+ await updateUserData(
2725
+ session.getUserId(),
2726
+ dataWithoutEmail,
2727
+ session.getTenantId()
2728
+ );
2679
2729
  }
2680
2730
  if (hasEmailUpdate) {
2681
2731
  const pendingVerificationResult = await startPendingEmailVerification({
@@ -2691,7 +2741,7 @@ function handleUpdateUser() {
2691
2741
  ...pendingVerificationResult
2692
2742
  };
2693
2743
  }
2694
- const user = await getUserById(session.getUserId());
2744
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2695
2745
  return {
2696
2746
  status: "OK",
2697
2747
  ...user
@@ -2758,7 +2808,7 @@ function handleGetUserField() {
2758
2808
  if (!field) {
2759
2809
  return missingFieldResponse();
2760
2810
  }
2761
- const user = await getUserById(session.getUserId());
2811
+ const user = await getUserById(session.getUserId(), session.getTenantId());
2762
2812
  return {
2763
2813
  status: "OK",
2764
2814
  value: user.data[field]
@@ -2792,9 +2842,11 @@ function handleUpdateUserField() {
2792
2842
  if (permissionError) {
2793
2843
  return permissionError;
2794
2844
  }
2795
- const updateUserDataResult = await updateUserData(session.getUserId(), {
2796
- [field]: payload.value
2797
- });
2845
+ const updateUserDataResult = await updateUserData(
2846
+ session.getUserId(),
2847
+ { [field]: payload.value },
2848
+ session.getTenantId()
2849
+ );
2798
2850
  return {
2799
2851
  status: "OK",
2800
2852
  ...updateUserDataResult
@@ -3277,6 +3329,7 @@ var init = createPluginInitFunction(
3277
3329
  const verificationResult = await completePendingEmailVerification({
3278
3330
  recipeUserId: response.user.recipeUserId,
3279
3331
  email: response.user.email,
3332
+ tenantId: input.tenantId,
3280
3333
  userContext: input.userContext
3281
3334
  });
3282
3335
  const session = input.session;
@@ -63,6 +63,8 @@ rownd:
63
63
  supertokens:
64
64
  connectionURI: <CONNECTION_URI>
65
65
  apiKey: <API_KEY>
66
+ # Tenant that receives every imported login method.
67
+ tenantId: public
66
68
  # Number of users to send to SuperTokens per bulk import request.
67
69
  batchSize: 500
68
70
  `;
@@ -69,7 +69,8 @@ var ConfigSchema = import_zod.z.object({
69
69
  supertokens: import_zod.z.object({
70
70
  connectionURI: import_zod.z.string(),
71
71
  apiKey: import_zod.z.string().optional(),
72
- batchSize: import_zod.z.number().int().positive()
72
+ batchSize: import_zod.z.number().int().positive(),
73
+ tenantId: import_zod.z.string().min(1).default("public")
73
74
  }),
74
75
  thirdPartyProviders: import_zod.z.array(
75
76
  import_zod.z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supertokens-plugins/rownd-nodejs",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Rownd User Migration Plugin for SuperTokens",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",