@supertokens-plugins/rownd-nodejs 0.3.0-beta.8 → 0.5.1

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
@@ -15,7 +15,7 @@ npm install @supertokens-plugins/rownd-nodejs
15
15
  Initialize the plugin in your SuperTokens backend configuration.
16
16
 
17
17
  > [!IMPORTANT]
18
- > This plugin requires the `Session` and `UserMetadata` recipes to be initialized in your SuperTokens configuration.
18
+ > This plugin always requires the `Session` and `UserMetadata` recipes. Enable `Passwordless` for email/phone, `ThirdParty` for Google/Apple/guest/anonymous users, `EmailVerification` for verified email profile updates, and `AccountLinking` when migrated Rownd users may have multiple supported login methods.
19
19
 
20
20
  ```typescript
21
21
  import SuperTokens from "supertokens-node";
@@ -128,17 +128,18 @@ const magicLink = await createMagicLinkWithConfirmationBypass({
128
128
 
129
129
  Pass exactly one of `email` or `phoneNumber`. The helper returns the rewritten magic link with `bypassDeviceConfirmation=true`.
130
130
 
131
- Before skipping the cross-device confirmation prompt, the frontend should validate the callback against the plugin:
131
+ 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
132
 
133
- - **POST** `/plugin/passwordless-cross-device-confirmation/validate`
133
+ - **POST** `{apiBasePath}/plugin/passwordless-cross-device-confirmation/validate`
134
+ - **Default**: `POST /auth/plugin/passwordless-cross-device-confirmation/validate`
134
135
  - **Body**: `{ "clientDomain": "browser", "redirectToPath": "/profile", "appVariantId": "optional_variant" }`
135
136
  - **Success response**: `{ "status": "OK", "bypass": true }`
136
137
 
137
138
  If validation fails, the frontend should show the normal cross-device confirmation prompt.
138
139
 
139
- ## API Endpoint
140
+ ## API Endpoints
140
141
 
141
- The plugin exposes a single endpoint:
142
+ 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/...`.
142
143
 
143
144
  > [!IMPORTANT]
144
145
  > The plugin always migrates users and sessions into the `public` tenant.
@@ -146,7 +147,8 @@ The plugin exposes a single endpoint:
146
147
 
147
148
  ### Migrate
148
149
 
149
- - **POST** `/plugin/rownd/migrate`
150
+ - **POST** `{apiBasePath}/plugin/rownd/migrate`
151
+ - **Default**: `POST /auth/plugin/rownd/migrate`
150
152
  - **Headers**: `Authorization: Bearer <Rownd_JWT>`. Header-token clients should also send `rid: session`, `fdi-version: 1.18`, and `st-auth-mode: header`.
151
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.
152
154
 
@@ -164,13 +166,11 @@ The plugin emits exactly one telemetry event per `/migrate` call result.
164
166
 
165
167
  Each event includes endpoint outcome data only (not step-by-step events), including:
166
168
 
167
- - `operation`: `migrate`
168
169
  - `outcome`: `success` or `error`
169
170
  - `durationMs`
170
171
  - `tenantId` (when available)
171
172
  - `rowndUserId` (when available)
172
173
  - `superTokensUserId` (when available)
173
- - `migrationState`: `already-migrated` or `imported-during-request` (when available)
174
174
  - for errors: `error.message` and `error.name`
175
175
 
176
176
  > [!NOTE]
@@ -228,20 +228,21 @@ RowndMigrationPlugin.init({
228
228
 
229
229
  The package includes a bulk migration script for importing Rownd users into SuperTokens.
230
230
 
231
- The script now runs directly from a YAML config file that lives beside the script:
232
-
233
- - config file: `packages/rownd-nodejs/scripts/config.yaml`
234
- - script: `packages/rownd-nodejs/scripts/bulkMigrate.ts`
231
+ The script runs from a YAML config file generated from the included template.
235
232
 
236
233
  ### Usage
237
234
 
238
- 1. Edit `scripts/config.yaml` with your Rownd and SuperTokens credentials.
239
- 2. Run the script from `packages/rownd-nodejs`.
235
+ 1. Generate a local config file.
236
+ 2. Edit the config with your Rownd and SuperTokens credentials.
237
+ 3. Run the migration.
240
238
 
241
239
  ```bash
242
- npm run bulk-import
240
+ npx rownd-nodejs init-config --output ./rownd-bulk-migrate.yaml
241
+ npx rownd-nodejs bulk-migrate --config ./rownd-bulk-migrate.yaml
243
242
  ```
244
243
 
244
+ For repo-local development, use `npm run cli -- bulk-migrate --config ./rownd-bulk-migrate.yaml` from `packages/rownd-nodejs`.
245
+
245
246
  The script:
246
247
 
247
248
  - fetches users from Rownd page by page
@@ -252,5 +253,5 @@ The script:
252
253
 
253
254
  ### Config File
254
255
 
255
- All runtime config is read from `scripts/config.yaml`.
256
+ All runtime config is read from the YAML file passed with `--config`.
256
257
  There is no environment variable parsing.
@@ -40,6 +40,7 @@ var fs2 = __toESM(require("fs/promises"));
40
40
  var import_zod2 = require("zod");
41
41
 
42
42
  // src/rownd-compatibility.ts
43
+ var import_node_crypto = require("crypto");
43
44
  var import_supertokens_node = __toESM(require("supertokens-node"));
44
45
  var import_usermetadata = __toESM(require("supertokens-node/recipe/usermetadata"));
45
46
 
@@ -55,6 +56,11 @@ var IDENTITY_USER_DATA_FIELDS = /* @__PURE__ */ new Set([
55
56
  "google_id",
56
57
  "apple_id"
57
58
  ]);
59
+ var SUPERTOKENS_FAKE_EMAIL_DOMAIN = "stfakeemail.supertokens.com";
60
+ function buildSuperTokensFakeEmail(thirdPartyUserId, thirdPartyId) {
61
+ const hash = (0, import_node_crypto.createHash)("sha256").update(`${thirdPartyId}:${thirdPartyUserId}`).digest("hex").slice(0, 32);
62
+ return `st-${thirdPartyId}-${hash}@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`;
63
+ }
58
64
  function isIdentityField(field) {
59
65
  return IDENTITY_USER_DATA_FIELDS.has(field);
60
66
  }
@@ -66,27 +72,23 @@ function mapRowndUserToSuperTokens(rowndUser) {
66
72
  throw new Error("Rownd user has no user_id");
67
73
  }
68
74
  if (rowndUserData.google_id) {
69
- if (!rowndUserData.email) {
70
- throw new Error("Rownd Google user is missing email");
71
- }
75
+ const googleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.google_id, "google");
72
76
  loginMethods.push({
73
77
  recipeId: "thirdparty",
74
78
  thirdPartyId: "google",
75
79
  thirdPartyUserId: rowndUserData.google_id,
76
- email: rowndUserData.email,
77
- isVerified: !!rowndUserVerifiedData.google_id
80
+ email: googleEmail,
81
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id
78
82
  });
79
83
  }
80
84
  if (rowndUserData.apple_id) {
81
- if (!rowndUserData.email) {
82
- throw new Error("Rownd Apple user is missing email");
83
- }
85
+ const appleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.apple_id, "apple");
84
86
  loginMethods.push({
85
87
  recipeId: "thirdparty",
86
88
  thirdPartyId: "apple",
87
89
  thirdPartyUserId: rowndUserData.apple_id,
88
- email: rowndUserData.email,
89
- isVerified: !!rowndUserVerifiedData.apple_id
90
+ email: appleEmail,
91
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id
90
92
  });
91
93
  }
92
94
  if (rowndUserData.phone_number) {
@@ -159,7 +161,9 @@ var ConfigSchema = import_zod.z.object({
159
161
  appKey: import_zod.z.string(),
160
162
  appSecret: import_zod.z.string(),
161
163
  bearerToken: import_zod.z.string().optional(),
162
- pageSize: import_zod.z.number().int().positive()
164
+ pageSize: import_zod.z.number().int().positive(),
165
+ oidcClientSecrets: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
166
+ provisionOidcClientIdAliases: import_zod.z.boolean().default(true)
163
167
  }),
164
168
  supertokens: import_zod.z.object({
165
169
  connectionURI: import_zod.z.string(),
@@ -61,7 +61,9 @@ var ConfigSchema = import_zod.z.object({
61
61
  appKey: import_zod.z.string(),
62
62
  appSecret: import_zod.z.string(),
63
63
  bearerToken: import_zod.z.string().optional(),
64
- pageSize: import_zod.z.number().int().positive()
64
+ pageSize: import_zod.z.number().int().positive(),
65
+ oidcClientSecrets: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
66
+ provisionOidcClientIdAliases: import_zod.z.boolean().default(true)
65
67
  }),
66
68
  supertokens: import_zod.z.object({
67
69
  connectionURI: import_zod.z.string(),
package/dist/index.js CHANGED
@@ -865,6 +865,7 @@ var RowndPluginError = class extends Error {
865
865
  };
866
866
 
867
867
  // src/utils.ts
868
+ var ROWND_OAUTH_LOGIN_CHALLENGE_PARAM = "rownd_oauth_login_challenge";
868
869
  function rewriteLinkPath(inputUrl, targetPath, searchParams) {
869
870
  try {
870
871
  const url = new URL(inputUrl);
@@ -1065,7 +1066,8 @@ function getMagicLinkBootstrapParams(input) {
1065
1066
  ...input.appVariantId ? { appVariantId: input.appVariantId } : {},
1066
1067
  ...input.displayContext ? { displayContext: input.displayContext } : {},
1067
1068
  ...input.redirectToPath ? { redirectToPath: input.redirectToPath } : {},
1068
- ...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {}
1069
+ ...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {},
1070
+ ...input.oauthLoginChallenge ? { oauthLoginChallenge: input.oauthLoginChallenge } : {}
1069
1071
  };
1070
1072
  }
1071
1073
  function rewriteMagicLink(input) {
@@ -1091,6 +1093,10 @@ function getRequestedRedirectToPathFromRequest(req) {
1091
1093
  const redirectToPath = req.getKeyValueFromQuery("rownd_redirect_to_path");
1092
1094
  return typeof redirectToPath === "string" && redirectToPath.length > 0 ? redirectToPath : void 0;
1093
1095
  }
1096
+ function getRequestedOAuthLoginChallengeFromRequest(req) {
1097
+ const loginChallenge = req.getKeyValueFromQuery(ROWND_OAUTH_LOGIN_CHALLENGE_PARAM);
1098
+ return typeof loginChallenge === "string" && loginChallenge.length > 0 ? loginChallenge : void 0;
1099
+ }
1094
1100
 
1095
1101
  // src/config.ts
1096
1102
  var pluginConfig;
@@ -1463,6 +1469,7 @@ function buildAuthMobileConfig(authMobile) {
1463
1469
  }
1464
1470
 
1465
1471
  // src/rownd-compatibility.ts
1472
+ var import_node_crypto = require("crypto");
1466
1473
  var import_supertokens_node = __toESM(require("supertokens-node"));
1467
1474
  var import_usermetadata2 = __toESM(require("supertokens-node/recipe/usermetadata"));
1468
1475
  var IDENTITY_USER_DATA_FIELDS = /* @__PURE__ */ new Set([
@@ -1476,6 +1483,14 @@ var INTERNAL_METADATA_FIELDS = /* @__PURE__ */ new Set([
1476
1483
  "original_rownd_user",
1477
1484
  "rownd_pending_verification"
1478
1485
  ]);
1486
+ var SUPERTOKENS_FAKE_EMAIL_DOMAIN = "stfakeemail.supertokens.com";
1487
+ function isSuperTokensFakeEmail(email) {
1488
+ return typeof email === "string" && email.toLowerCase().endsWith(`@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`);
1489
+ }
1490
+ function buildSuperTokensFakeEmail(thirdPartyUserId, thirdPartyId) {
1491
+ const hash = (0, import_node_crypto.createHash)("sha256").update(`${thirdPartyId}:${thirdPartyUserId}`).digest("hex").slice(0, 32);
1492
+ return `st-${thirdPartyId}-${hash}@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`;
1493
+ }
1479
1494
  function isIdentityField(field) {
1480
1495
  return IDENTITY_USER_DATA_FIELDS.has(field);
1481
1496
  }
@@ -1490,27 +1505,23 @@ function mapRowndUserToSuperTokens(rowndUser) {
1490
1505
  throw new Error("Rownd user has no user_id");
1491
1506
  }
1492
1507
  if (rowndUserData.google_id) {
1493
- if (!rowndUserData.email) {
1494
- throw new Error("Rownd Google user is missing email");
1495
- }
1508
+ const googleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.google_id, "google");
1496
1509
  loginMethods.push({
1497
1510
  recipeId: "thirdparty",
1498
1511
  thirdPartyId: "google",
1499
1512
  thirdPartyUserId: rowndUserData.google_id,
1500
- email: rowndUserData.email,
1501
- isVerified: !!rowndUserVerifiedData.google_id
1513
+ email: googleEmail,
1514
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id
1502
1515
  });
1503
1516
  }
1504
1517
  if (rowndUserData.apple_id) {
1505
- if (!rowndUserData.email) {
1506
- throw new Error("Rownd Apple user is missing email");
1507
- }
1518
+ const appleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.apple_id, "apple");
1508
1519
  loginMethods.push({
1509
1520
  recipeId: "thirdparty",
1510
1521
  thirdPartyId: "apple",
1511
1522
  thirdPartyUserId: rowndUserData.apple_id,
1512
- email: rowndUserData.email,
1513
- isVerified: !!rowndUserVerifiedData.apple_id
1523
+ email: appleEmail,
1524
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id
1514
1525
  });
1515
1526
  }
1516
1527
  if (rowndUserData.phone_number) {
@@ -1695,7 +1706,7 @@ async function buildStandardOAuthClaims(user, scopes) {
1695
1706
  const rowndData = isJsonRecord(metadata.original_rownd_user?.data) ? metadata.original_rownd_user.data : {};
1696
1707
  const verifiedData = isJsonRecord(metadata.original_rownd_user?.verified_data) ? metadata.original_rownd_user.verified_data : {};
1697
1708
  if (scopes.includes("email")) {
1698
- const email = firstString(rowndData.email) ?? user.emails[0];
1709
+ const email = firstRealEmail(firstString(rowndData.email), ...user.emails);
1699
1710
  if (email) {
1700
1711
  claims.email = email;
1701
1712
  claims.email_verified = isOAuthClaimVerified(
@@ -1762,6 +1773,11 @@ function firstString(value) {
1762
1773
  }
1763
1774
  return typeof value === "string" && value.length > 0 ? value : void 0;
1764
1775
  }
1776
+ function firstRealEmail(...values) {
1777
+ return values.find((entry) => {
1778
+ return typeof entry === "string" && entry.length > 0 && !isSuperTokensFakeEmail(entry);
1779
+ });
1780
+ }
1765
1781
  function isOAuthClaimVerified(value, expectedValue, fallback) {
1766
1782
  return value === true || value === expectedValue || fallback;
1767
1783
  }
@@ -2045,6 +2061,7 @@ async function createMagicLinkWithConfirmationBypass(input) {
2045
2061
  })}${getAppInfoString(stConfig.appInfo.websiteBasePath)}/verify?preAuthSessionId=${encodeURIComponent(
2046
2062
  codeInfo.preAuthSessionId
2047
2063
  )}&tenantId=${encodeURIComponent(tenantId)}#${encodeURIComponent(codeInfo.linkCode)}`;
2064
+ const oauthLoginChallenge = userContext.rowndOAuthLoginChallenge;
2048
2065
  const rewrittenUrl = new URL(rewriteMagicLink({
2049
2066
  magicLink,
2050
2067
  clientDomain,
@@ -2055,7 +2072,8 @@ async function createMagicLinkWithConfirmationBypass(input) {
2055
2072
  appVariantId,
2056
2073
  displayContext: input.displayContext,
2057
2074
  redirectToPath,
2058
- clientDomainKey: input.clientDomain
2075
+ clientDomainKey: input.clientDomain,
2076
+ oauthLoginChallenge: typeof oauthLoginChallenge === "string" ? oauthLoginChallenge : void 0
2059
2077
  })
2060
2078
  }));
2061
2079
  rewrittenUrl.searchParams.set(PASSWORDLESS_BYPASS_DEVICE_CONFIRMATION_PARAM, "true");
@@ -2106,7 +2124,7 @@ async function getUserById(userId) {
2106
2124
  };
2107
2125
  for (const method of stUser.loginMethods) {
2108
2126
  if (method.recipeId === "passwordless") {
2109
- if (method.email) {
2127
+ if (method.email && !isSuperTokensFakeEmail(method.email)) {
2110
2128
  verifiedData.email = method.email;
2111
2129
  if (data.email === void 0) data.email = method.email;
2112
2130
  }
@@ -2118,10 +2136,10 @@ async function getUserById(userId) {
2118
2136
  } else if (method.recipeId === "thirdparty") {
2119
2137
  const thirdPartyId = getThirdPartyId(method);
2120
2138
  const thirdPartyUserId = getThirdPartyUserId(method);
2121
- if (method.verified && method.email) {
2139
+ if (method.verified && method.email && !isSuperTokensFakeEmail(method.email)) {
2122
2140
  verifiedData.email = method.email;
2123
2141
  }
2124
- if (method.email && data.email === void 0) {
2142
+ if (method.email && !isSuperTokensFakeEmail(method.email) && data.email === void 0) {
2125
2143
  data.email = method.email;
2126
2144
  }
2127
2145
  if (thirdPartyId === "google" && thirdPartyUserId) {
@@ -2133,7 +2151,7 @@ async function getUserById(userId) {
2133
2151
  verifiedData.apple_id = thirdPartyUserId;
2134
2152
  }
2135
2153
  } else if (method.recipeId === "emailpassword") {
2136
- if (method.email && data.email === void 0) {
2154
+ if (method.email && !isSuperTokensFakeEmail(method.email) && data.email === void 0) {
2137
2155
  data.email = method.email;
2138
2156
  }
2139
2157
  }
@@ -2838,6 +2856,7 @@ var init = createPluginInitFunction(
2838
2856
  const displayContext = input?.userContext?.rowndDisplayContext;
2839
2857
  const redirectToPath = input?.userContext?.rowndRedirectToPath;
2840
2858
  const clientDomain = input?.userContext?.rowndClientDomain;
2859
+ const oauthLoginChallenge = input?.userContext?.rowndOAuthLoginChallenge;
2841
2860
  const clientDomainKey = clientDomain ?? (displayContext === "mobile_app" ? "mobile" : "browser");
2842
2861
  const clientBaseUrl = pluginConfig2.clientDomains?.[clientDomainKey];
2843
2862
  const bootstrapParams = {
@@ -2845,7 +2864,8 @@ var init = createPluginInitFunction(
2845
2864
  ...hubBootstrapParams ?? {},
2846
2865
  ...typeof appVariantId === "string" ? { appVariantId } : {},
2847
2866
  ...typeof displayContext === "string" ? { displayContext } : {},
2848
- ...typeof redirectToPath === "string" ? { redirectToPath } : {}
2867
+ ...typeof redirectToPath === "string" ? { redirectToPath } : {},
2868
+ ...typeof oauthLoginChallenge === "string" ? { oauthLoginChallenge } : {}
2849
2869
  };
2850
2870
  const rewrittenLink = input[linkKey] ? clientBaseUrl ? rewriteLinkToBaseUrl(
2851
2871
  input[linkKey],
@@ -3131,6 +3151,12 @@ var init = createPluginInitFunction(
3131
3151
  if (appVariantId) {
3132
3152
  input.userContext.rowndAppVariantId = appVariantId;
3133
3153
  }
3154
+ const oauthLoginChallenge = getRequestedOAuthLoginChallengeFromRequest(
3155
+ input.options.req
3156
+ );
3157
+ if (oauthLoginChallenge) {
3158
+ input.userContext.rowndOAuthLoginChallenge = oauthLoginChallenge;
3159
+ }
3134
3160
  assertRowndAppVariantIsConfigured(appVariantId);
3135
3161
  return originalImplementation.createCodePOST(input);
3136
3162
  },
package/dist/index.mjs CHANGED
@@ -842,6 +842,7 @@ var RowndPluginError = class extends Error {
842
842
  };
843
843
 
844
844
  // src/utils.ts
845
+ var ROWND_OAUTH_LOGIN_CHALLENGE_PARAM = "rownd_oauth_login_challenge";
845
846
  function rewriteLinkPath(inputUrl, targetPath, searchParams) {
846
847
  try {
847
848
  const url = new URL(inputUrl);
@@ -1042,7 +1043,8 @@ function getMagicLinkBootstrapParams(input) {
1042
1043
  ...input.appVariantId ? { appVariantId: input.appVariantId } : {},
1043
1044
  ...input.displayContext ? { displayContext: input.displayContext } : {},
1044
1045
  ...input.redirectToPath ? { redirectToPath: input.redirectToPath } : {},
1045
- ...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {}
1046
+ ...input.clientDomainKey ? { clientDomain: input.clientDomainKey } : {},
1047
+ ...input.oauthLoginChallenge ? { oauthLoginChallenge: input.oauthLoginChallenge } : {}
1046
1048
  };
1047
1049
  }
1048
1050
  function rewriteMagicLink(input) {
@@ -1068,6 +1070,10 @@ function getRequestedRedirectToPathFromRequest(req) {
1068
1070
  const redirectToPath = req.getKeyValueFromQuery("rownd_redirect_to_path");
1069
1071
  return typeof redirectToPath === "string" && redirectToPath.length > 0 ? redirectToPath : void 0;
1070
1072
  }
1073
+ function getRequestedOAuthLoginChallengeFromRequest(req) {
1074
+ const loginChallenge = req.getKeyValueFromQuery(ROWND_OAUTH_LOGIN_CHALLENGE_PARAM);
1075
+ return typeof loginChallenge === "string" && loginChallenge.length > 0 ? loginChallenge : void 0;
1076
+ }
1071
1077
 
1072
1078
  // src/config.ts
1073
1079
  var pluginConfig;
@@ -1440,6 +1446,7 @@ function buildAuthMobileConfig(authMobile) {
1440
1446
  }
1441
1447
 
1442
1448
  // src/rownd-compatibility.ts
1449
+ import { createHash } from "crypto";
1443
1450
  import SuperTokens from "supertokens-node";
1444
1451
  import UserMetadata2 from "supertokens-node/recipe/usermetadata";
1445
1452
  var IDENTITY_USER_DATA_FIELDS = /* @__PURE__ */ new Set([
@@ -1453,6 +1460,14 @@ var INTERNAL_METADATA_FIELDS = /* @__PURE__ */ new Set([
1453
1460
  "original_rownd_user",
1454
1461
  "rownd_pending_verification"
1455
1462
  ]);
1463
+ var SUPERTOKENS_FAKE_EMAIL_DOMAIN = "stfakeemail.supertokens.com";
1464
+ function isSuperTokensFakeEmail(email) {
1465
+ return typeof email === "string" && email.toLowerCase().endsWith(`@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`);
1466
+ }
1467
+ function buildSuperTokensFakeEmail(thirdPartyUserId, thirdPartyId) {
1468
+ const hash = createHash("sha256").update(`${thirdPartyId}:${thirdPartyUserId}`).digest("hex").slice(0, 32);
1469
+ return `st-${thirdPartyId}-${hash}@${SUPERTOKENS_FAKE_EMAIL_DOMAIN}`;
1470
+ }
1456
1471
  function isIdentityField(field) {
1457
1472
  return IDENTITY_USER_DATA_FIELDS.has(field);
1458
1473
  }
@@ -1467,27 +1482,23 @@ function mapRowndUserToSuperTokens(rowndUser) {
1467
1482
  throw new Error("Rownd user has no user_id");
1468
1483
  }
1469
1484
  if (rowndUserData.google_id) {
1470
- if (!rowndUserData.email) {
1471
- throw new Error("Rownd Google user is missing email");
1472
- }
1485
+ const googleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.google_id, "google");
1473
1486
  loginMethods.push({
1474
1487
  recipeId: "thirdparty",
1475
1488
  thirdPartyId: "google",
1476
1489
  thirdPartyUserId: rowndUserData.google_id,
1477
- email: rowndUserData.email,
1478
- isVerified: !!rowndUserVerifiedData.google_id
1490
+ email: googleEmail,
1491
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.google_id
1479
1492
  });
1480
1493
  }
1481
1494
  if (rowndUserData.apple_id) {
1482
- if (!rowndUserData.email) {
1483
- throw new Error("Rownd Apple user is missing email");
1484
- }
1495
+ const appleEmail = rowndUserData.email ?? buildSuperTokensFakeEmail(rowndUserData.apple_id, "apple");
1485
1496
  loginMethods.push({
1486
1497
  recipeId: "thirdparty",
1487
1498
  thirdPartyId: "apple",
1488
1499
  thirdPartyUserId: rowndUserData.apple_id,
1489
- email: rowndUserData.email,
1490
- isVerified: !!rowndUserVerifiedData.apple_id
1500
+ email: appleEmail,
1501
+ isVerified: !!rowndUserData.email && !!rowndUserVerifiedData.apple_id
1491
1502
  });
1492
1503
  }
1493
1504
  if (rowndUserData.phone_number) {
@@ -1672,7 +1683,7 @@ async function buildStandardOAuthClaims(user, scopes) {
1672
1683
  const rowndData = isJsonRecord(metadata.original_rownd_user?.data) ? metadata.original_rownd_user.data : {};
1673
1684
  const verifiedData = isJsonRecord(metadata.original_rownd_user?.verified_data) ? metadata.original_rownd_user.verified_data : {};
1674
1685
  if (scopes.includes("email")) {
1675
- const email = firstString(rowndData.email) ?? user.emails[0];
1686
+ const email = firstRealEmail(firstString(rowndData.email), ...user.emails);
1676
1687
  if (email) {
1677
1688
  claims.email = email;
1678
1689
  claims.email_verified = isOAuthClaimVerified(
@@ -1739,6 +1750,11 @@ function firstString(value) {
1739
1750
  }
1740
1751
  return typeof value === "string" && value.length > 0 ? value : void 0;
1741
1752
  }
1753
+ function firstRealEmail(...values) {
1754
+ return values.find((entry) => {
1755
+ return typeof entry === "string" && entry.length > 0 && !isSuperTokensFakeEmail(entry);
1756
+ });
1757
+ }
1742
1758
  function isOAuthClaimVerified(value, expectedValue, fallback) {
1743
1759
  return value === true || value === expectedValue || fallback;
1744
1760
  }
@@ -2022,6 +2038,7 @@ async function createMagicLinkWithConfirmationBypass(input) {
2022
2038
  })}${getAppInfoString(stConfig.appInfo.websiteBasePath)}/verify?preAuthSessionId=${encodeURIComponent(
2023
2039
  codeInfo.preAuthSessionId
2024
2040
  )}&tenantId=${encodeURIComponent(tenantId)}#${encodeURIComponent(codeInfo.linkCode)}`;
2041
+ const oauthLoginChallenge = userContext.rowndOAuthLoginChallenge;
2025
2042
  const rewrittenUrl = new URL(rewriteMagicLink({
2026
2043
  magicLink,
2027
2044
  clientDomain,
@@ -2032,7 +2049,8 @@ async function createMagicLinkWithConfirmationBypass(input) {
2032
2049
  appVariantId,
2033
2050
  displayContext: input.displayContext,
2034
2051
  redirectToPath,
2035
- clientDomainKey: input.clientDomain
2052
+ clientDomainKey: input.clientDomain,
2053
+ oauthLoginChallenge: typeof oauthLoginChallenge === "string" ? oauthLoginChallenge : void 0
2036
2054
  })
2037
2055
  }));
2038
2056
  rewrittenUrl.searchParams.set(PASSWORDLESS_BYPASS_DEVICE_CONFIRMATION_PARAM, "true");
@@ -2083,7 +2101,7 @@ async function getUserById(userId) {
2083
2101
  };
2084
2102
  for (const method of stUser.loginMethods) {
2085
2103
  if (method.recipeId === "passwordless") {
2086
- if (method.email) {
2104
+ if (method.email && !isSuperTokensFakeEmail(method.email)) {
2087
2105
  verifiedData.email = method.email;
2088
2106
  if (data.email === void 0) data.email = method.email;
2089
2107
  }
@@ -2095,10 +2113,10 @@ async function getUserById(userId) {
2095
2113
  } else if (method.recipeId === "thirdparty") {
2096
2114
  const thirdPartyId = getThirdPartyId(method);
2097
2115
  const thirdPartyUserId = getThirdPartyUserId(method);
2098
- if (method.verified && method.email) {
2116
+ if (method.verified && method.email && !isSuperTokensFakeEmail(method.email)) {
2099
2117
  verifiedData.email = method.email;
2100
2118
  }
2101
- if (method.email && data.email === void 0) {
2119
+ if (method.email && !isSuperTokensFakeEmail(method.email) && data.email === void 0) {
2102
2120
  data.email = method.email;
2103
2121
  }
2104
2122
  if (thirdPartyId === "google" && thirdPartyUserId) {
@@ -2110,7 +2128,7 @@ async function getUserById(userId) {
2110
2128
  verifiedData.apple_id = thirdPartyUserId;
2111
2129
  }
2112
2130
  } else if (method.recipeId === "emailpassword") {
2113
- if (method.email && data.email === void 0) {
2131
+ if (method.email && !isSuperTokensFakeEmail(method.email) && data.email === void 0) {
2114
2132
  data.email = method.email;
2115
2133
  }
2116
2134
  }
@@ -2815,6 +2833,7 @@ var init = createPluginInitFunction(
2815
2833
  const displayContext = input?.userContext?.rowndDisplayContext;
2816
2834
  const redirectToPath = input?.userContext?.rowndRedirectToPath;
2817
2835
  const clientDomain = input?.userContext?.rowndClientDomain;
2836
+ const oauthLoginChallenge = input?.userContext?.rowndOAuthLoginChallenge;
2818
2837
  const clientDomainKey = clientDomain ?? (displayContext === "mobile_app" ? "mobile" : "browser");
2819
2838
  const clientBaseUrl = pluginConfig2.clientDomains?.[clientDomainKey];
2820
2839
  const bootstrapParams = {
@@ -2822,7 +2841,8 @@ var init = createPluginInitFunction(
2822
2841
  ...hubBootstrapParams ?? {},
2823
2842
  ...typeof appVariantId === "string" ? { appVariantId } : {},
2824
2843
  ...typeof displayContext === "string" ? { displayContext } : {},
2825
- ...typeof redirectToPath === "string" ? { redirectToPath } : {}
2844
+ ...typeof redirectToPath === "string" ? { redirectToPath } : {},
2845
+ ...typeof oauthLoginChallenge === "string" ? { oauthLoginChallenge } : {}
2826
2846
  };
2827
2847
  const rewrittenLink = input[linkKey] ? clientBaseUrl ? rewriteLinkToBaseUrl(
2828
2848
  input[linkKey],
@@ -3108,6 +3128,12 @@ var init = createPluginInitFunction(
3108
3128
  if (appVariantId) {
3109
3129
  input.userContext.rowndAppVariantId = appVariantId;
3110
3130
  }
3131
+ const oauthLoginChallenge = getRequestedOAuthLoginChallengeFromRequest(
3132
+ input.options.req
3133
+ );
3134
+ if (oauthLoginChallenge) {
3135
+ input.userContext.rowndOAuthLoginChallenge = oauthLoginChallenge;
3136
+ }
3111
3137
  assertRowndAppVariantIsConfigured(appVariantId);
3112
3138
  return originalImplementation.createCodePOST(input);
3113
3139
  },
@@ -49,6 +49,16 @@ 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>
@@ -62,7 +62,9 @@ var ConfigSchema = import_zod.z.object({
62
62
  appKey: import_zod.z.string(),
63
63
  appSecret: import_zod.z.string(),
64
64
  bearerToken: import_zod.z.string().optional(),
65
- pageSize: import_zod.z.number().int().positive()
65
+ pageSize: import_zod.z.number().int().positive(),
66
+ oidcClientSecrets: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
67
+ provisionOidcClientIdAliases: import_zod.z.boolean().default(true)
66
68
  }),
67
69
  supertokens: import_zod.z.object({
68
70
  connectionURI: import_zod.z.string(),
@@ -187,55 +189,118 @@ async function provisionSuperTokensInfrastructure(config, oidcClients) {
187
189
  if (config.supertokens.apiKey) {
188
190
  headers["api-key"] = config.supertokens.apiKey;
189
191
  }
192
+ const provisionedClientIds = /* @__PURE__ */ new Set();
190
193
  for (const oidcClient of oidcClients) {
191
- for (const credential of oidcClient.credentials ?? []) {
192
- console.log(
193
- `Provisioning SuperTokens OAuth2 Client: ${credential.client_id}`
194
+ const credentials = oidcClient.credentials ?? [];
195
+ for (const credential of credentials) {
196
+ await createSuperTokensOAuthClient({
197
+ config,
198
+ headers,
199
+ oidcClient,
200
+ clientId: credential.client_id,
201
+ clientSecret: resolveOidcClientSecret(
202
+ config,
203
+ credential.client_id,
204
+ credential.secret
205
+ ),
206
+ appVariantId: credential.app_variant_id,
207
+ provisionedClientIds
208
+ });
209
+ }
210
+ if (config.rownd.provisionOidcClientIdAliases && credentials.length > 0) {
211
+ const firstCredential = credentials[0];
212
+ if (!firstCredential) {
213
+ continue;
214
+ }
215
+ const firstCredentialSecret = resolveOidcClientSecret(
216
+ config,
217
+ firstCredential.client_id,
218
+ firstCredential.secret
194
219
  );
195
- const oauthRes = await fetch(
196
- new URL(
197
- "/recipe/oauth/clients",
198
- config.supertokens.connectionURI
199
- ).toString(),
200
- {
201
- method: "POST",
202
- headers,
203
- body: JSON.stringify({
204
- clientName: oidcClient.name || credential.client_id || "Rownd Client",
205
- clientId: credential.client_id,
206
- clientSecret: credential.secret,
207
- redirectUris: oidcClient.config.redirect_uris || [],
208
- postLogoutRedirectUris: oidcClient.config.post_logout_uris || [],
209
- scope: oidcClient.config.allowed_scopes?.join(" "),
210
- responseTypes: ["code", "token", "id_token"],
211
- grantTypes: [
212
- "authorization_code",
213
- "refresh_token",
214
- "client_credentials",
215
- "implicit"
216
- ],
217
- tokenEndpointAuthMethod: oidcClient.config.token_endpoint_auth_method ?? "client_secret_basic",
218
- audience: [`app:${config.rownd.appId}`],
219
- metadata: {
220
- rowndOidcClientId: oidcClient.id,
221
- rowndAppVariantId: credential.app_variant_id || void 0,
222
- rowndAllowedScopes: oidcClient.config.allowed_scopes || [],
223
- rowndApplicationType: oidcClient.config.application_type,
224
- rowndIsPkceRequired: oidcClient.config.is_pkce_required,
225
- rowndIsPkceSupported: oidcClient.config.is_pkce_supported,
226
- rowndDeviceFlowEnabled: oidcClient.config.device_flow_enabled
227
- }
228
- })
229
- }
220
+ const aliasSecret = resolveOidcClientSecret(
221
+ config,
222
+ oidcClient.id,
223
+ firstCredentialSecret
230
224
  );
231
- if (!oauthRes.ok) {
232
- const errorText = await oauthRes.text();
233
- if (!errorText.includes("already exists") && !errorText.includes("Duplicate")) {
234
- throw new Error(
235
- `Failed to create OAuth2 Client ${credential.client_id}: ${oauthRes.status} ${errorText}`
236
- );
225
+ await createSuperTokensOAuthClient({
226
+ config,
227
+ headers,
228
+ oidcClient,
229
+ clientId: oidcClient.id,
230
+ clientSecret: aliasSecret,
231
+ isOidcClientIdAlias: true,
232
+ provisionedClientIds
233
+ });
234
+ }
235
+ }
236
+ }
237
+ function resolveOidcClientSecret(config, clientId, apiSecret) {
238
+ const secret = config.rownd.oidcClientSecrets?.[clientId] ?? apiSecret;
239
+ if (!secret) {
240
+ throw new Error(
241
+ `Missing plaintext secret for Rownd OIDC client ${clientId}. Add rownd.oidcClientSecrets.${clientId} to the config.`
242
+ );
243
+ }
244
+ if (secret.startsWith("$argon2")) {
245
+ throw new Error(
246
+ `Rownd returned a hashed secret for OIDC client ${clientId}. Add the plaintext secret under rownd.oidcClientSecrets.${clientId}.`
247
+ );
248
+ }
249
+ return secret;
250
+ }
251
+ async function createSuperTokensOAuthClient(input) {
252
+ if (input.provisionedClientIds.has(input.clientId)) {
253
+ return;
254
+ }
255
+ input.provisionedClientIds.add(input.clientId);
256
+ console.log(`Provisioning SuperTokens OAuth2 Client: ${input.clientId}`);
257
+ const grantTypes = [
258
+ "authorization_code",
259
+ "refresh_token",
260
+ "client_credentials",
261
+ "implicit"
262
+ ];
263
+ if (input.oidcClient.config.device_flow_enabled) {
264
+ grantTypes.push("urn:ietf:params:oauth:grant-type:device_code");
265
+ }
266
+ const oauthRes = await fetch(
267
+ new URL(
268
+ "/recipe/oauth/clients",
269
+ input.config.supertokens.connectionURI
270
+ ).toString(),
271
+ {
272
+ method: "POST",
273
+ headers: input.headers,
274
+ body: JSON.stringify({
275
+ clientName: input.oidcClient.name || input.clientId || "Rownd Client",
276
+ clientId: input.clientId,
277
+ clientSecret: input.clientSecret,
278
+ redirectUris: input.oidcClient.config.redirect_uris || [],
279
+ postLogoutRedirectUris: input.oidcClient.config.post_logout_uris || [],
280
+ scope: input.oidcClient.config.allowed_scopes?.join(" "),
281
+ responseTypes: ["code", "token", "id_token"],
282
+ grantTypes,
283
+ tokenEndpointAuthMethod: input.oidcClient.config.token_endpoint_auth_method ?? "client_secret_basic",
284
+ audience: [`app:${input.config.rownd.appId}`],
285
+ metadata: {
286
+ rowndOidcClientId: input.oidcClient.id,
287
+ rowndAppVariantId: input.appVariantId || void 0,
288
+ rowndAllowedScopes: input.oidcClient.config.allowed_scopes || [],
289
+ rowndApplicationType: input.oidcClient.config.application_type,
290
+ rowndIsPkceRequired: input.oidcClient.config.is_pkce_required,
291
+ rowndIsPkceSupported: input.oidcClient.config.is_pkce_supported,
292
+ rowndDeviceFlowEnabled: input.oidcClient.config.device_flow_enabled,
293
+ rowndIsOidcClientIdAlias: input.isOidcClientIdAlias || void 0
237
294
  }
238
- }
295
+ })
296
+ }
297
+ );
298
+ if (!oauthRes.ok) {
299
+ const errorText = await oauthRes.text();
300
+ if (!errorText.includes("already exists") && !errorText.includes("Duplicate")) {
301
+ throw new Error(
302
+ `Failed to create OAuth2 Client ${input.clientId}: ${oauthRes.status} ${errorText}`
303
+ );
239
304
  }
240
305
  }
241
306
  }
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@supertokens-plugins/rownd-nodejs",
3
- "version": "0.3.0-beta.8",
3
+ "version": "0.5.1",
4
4
  "description": "Rownd User Migration Plugin for SuperTokens",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
8
16
  "bin": {
9
17
  "rownd-nodejs": "./dist/cli.js"
10
18
  },
@@ -15,6 +23,8 @@
15
23
  "build": "rm -rf dist && tsup",
16
24
  "cli": "npm run build && node dist/cli.js",
17
25
  "lint": "eslint ./src --ext .ts",
26
+ "prepack": "npm run build",
27
+ "typecheck": "tsc --noEmit",
18
28
  "test": "TEST_MODE=testing vitest run --pool=forks"
19
29
  },
20
30
  "keywords": [