@supertokens-plugins/rownd-nodejs 0.3.0-beta.7 → 0.5.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
@@ -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.
package/dist/index.js CHANGED
@@ -289,6 +289,10 @@ __export(index_exports, {
289
289
  });
290
290
  module.exports = __toCommonJS(index_exports);
291
291
 
292
+ // src/plugin.ts
293
+ var import_emailverification2 = require("supertokens-node/recipe/emailverification");
294
+ var import_session3 = __toESM(require("supertokens-node/recipe/session"));
295
+
292
296
  // ../../shared/js/src/createPluginInit.ts
293
297
  var import_supertokens_js_override = __toESM(require_build());
294
298
  var createPluginInitFunction = (init2, getImplementation, getNormalisedConfig = (config2) => config2) => {
@@ -635,11 +639,12 @@ function createLoggerConfig() {
635
639
  }
636
640
  var config = createLoggerConfig();
637
641
  var logger = common_default(config);
638
- if (logger.formatters) {
639
- logger.formatters.o = function(value) {
642
+ var formatters = logger.formatters;
643
+ if (formatters) {
644
+ formatters.o = function(value) {
640
645
  return import_util.default.inspect(value, { colors: this.useColors, compact: true }).split("\n").map((line) => line.trim()).join(" ");
641
646
  };
642
- logger.formatters.O = function(value) {
647
+ formatters.O = function(value) {
643
648
  return import_util.default.inspect(value, { colors: this.useColors, compact: false });
644
649
  };
645
650
  }
@@ -1870,12 +1875,12 @@ function hasVerifiedRealLoginMethod(user) {
1870
1875
  });
1871
1876
  }
1872
1877
  function getEffectiveAuthLevel(user, originalAuthLevel, verifiedData) {
1873
- if (originalAuthLevel === INSTANT_AUTH_METHOD_ID) {
1874
- return INSTANT_AUTH_METHOD_ID;
1875
- }
1876
1878
  if (hasVerifiedRealLoginMethod(user)) {
1877
1879
  return "verified";
1878
1880
  }
1881
+ if (originalAuthLevel === INSTANT_AUTH_METHOD_ID) {
1882
+ return INSTANT_AUTH_METHOD_ID;
1883
+ }
1879
1884
  return getGuestAuthLevel(user) || originalAuthLevel || (verifiedData && Object.keys(verifiedData).length > 0 ? "verified" : "unverified");
1880
1885
  }
1881
1886
  function canUpdateUserDataField(field) {
@@ -2304,6 +2309,7 @@ async function completePendingEmailVerification(input) {
2304
2309
  return;
2305
2310
  }
2306
2311
  let metadataUserId = userId;
2312
+ let verifiedRecipeUserId = input.recipeUserId;
2307
2313
  const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(user);
2308
2314
  if (passwordlessEmailMethod) {
2309
2315
  const updateResult = await import_passwordless.default.updateUser({
@@ -2316,6 +2322,7 @@ async function completePendingEmailVerification(input) {
2316
2322
  `Failed to update verified email method: ${updateResult.status}`
2317
2323
  );
2318
2324
  }
2325
+ verifiedRecipeUserId = passwordlessEmailMethod.recipeUserId;
2319
2326
  } else if (hasOnlyGuestLoginMethods(user)) {
2320
2327
  const isPasswordlessSignUpAllowed = await import_accountlinking.default.isSignUpAllowed(
2321
2328
  PUBLIC_TENANT_ID,
@@ -2335,6 +2342,7 @@ async function completePendingEmailVerification(input) {
2335
2342
  tenantId: PUBLIC_TENANT_ID,
2336
2343
  userContext: input.userContext
2337
2344
  });
2345
+ verifiedRecipeUserId = passwordlessUser.recipeUserId;
2338
2346
  const primaryUserResult = await import_accountlinking.default.createPrimaryUser(
2339
2347
  passwordlessUser.recipeUserId,
2340
2348
  input.userContext
@@ -2380,6 +2388,10 @@ async function completePendingEmailVerification(input) {
2380
2388
  )
2381
2389
  };
2382
2390
  await import_usermetadata3.default.updateUserMetadata(metadataUserId, updatedMetadata);
2391
+ return {
2392
+ userId: metadataUserId,
2393
+ recipeUserId: verifiedRecipeUserId
2394
+ };
2383
2395
  }
2384
2396
  function isMatchingPendingEmailVerification(verification, email) {
2385
2397
  return verification.field === "email" && verification.value === email;
@@ -2808,6 +2820,12 @@ function validateWritableFields(fields) {
2808
2820
  }
2809
2821
 
2810
2822
  // src/plugin.ts
2823
+ var verifyRowndUserSessionOptions = {
2824
+ sessionRequired: true,
2825
+ overrideGlobalClaimValidators: (validators) => validators.filter((validator) => {
2826
+ return !("claim" in validator) || validator.claim.key !== import_emailverification2.EmailVerificationClaim.key;
2827
+ })
2828
+ };
2811
2829
  var init = createPluginInitFunction(
2812
2830
  (pluginConfig2) => {
2813
2831
  const rowndClient2 = (0, import_node.createInstance)({
@@ -2923,13 +2941,13 @@ var init = createPluginInitFunction(
2923
2941
  {
2924
2942
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user`,
2925
2943
  method: "get",
2926
- verifySessionOptions: { sessionRequired: true },
2944
+ verifySessionOptions: verifyRowndUserSessionOptions,
2927
2945
  handler: withRequestHandler(handleGetUser())
2928
2946
  },
2929
2947
  {
2930
2948
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user`,
2931
2949
  method: "put",
2932
- verifySessionOptions: { sessionRequired: true },
2950
+ verifySessionOptions: verifyRowndUserSessionOptions,
2933
2951
  handler: withRequestHandler(handleUpdateUser())
2934
2952
  },
2935
2953
  {
@@ -2953,13 +2971,13 @@ var init = createPluginInitFunction(
2953
2971
  {
2954
2972
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user/field`,
2955
2973
  method: "get",
2956
- verifySessionOptions: { sessionRequired: true },
2974
+ verifySessionOptions: verifyRowndUserSessionOptions,
2957
2975
  handler: withRequestHandler(handleGetUserField())
2958
2976
  },
2959
2977
  {
2960
2978
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user/field`,
2961
2979
  method: "put",
2962
- verifySessionOptions: { sessionRequired: true },
2980
+ verifySessionOptions: verifyRowndUserSessionOptions,
2963
2981
  handler: withRequestHandler(handleUpdateUserField())
2964
2982
  }
2965
2983
  ]
@@ -3254,11 +3272,25 @@ var init = createPluginInitFunction(
3254
3272
  }
3255
3273
  const response = await originalImplementation.verifyEmailPOST(input);
3256
3274
  if (response.status === "OK") {
3257
- await completePendingEmailVerification({
3275
+ const verificationResult = await completePendingEmailVerification({
3258
3276
  recipeUserId: response.user.recipeUserId,
3259
3277
  email: response.user.email,
3260
3278
  userContext: input.userContext
3261
3279
  });
3280
+ const session = input.session;
3281
+ const shouldReplaceSession = session && verificationResult && (session.getUserId(input.userContext) !== verificationResult.userId || session.getRecipeUserId(input.userContext).getAsString() !== verificationResult.recipeUserId.getAsString());
3282
+ if (shouldReplaceSession) {
3283
+ await session.revokeSession(input.userContext);
3284
+ response.newSession = await import_session3.default.createNewSession(
3285
+ input.options.req,
3286
+ input.options.res,
3287
+ session.getTenantId(input.userContext),
3288
+ verificationResult.recipeUserId,
3289
+ {},
3290
+ {},
3291
+ input.userContext
3292
+ );
3293
+ }
3262
3294
  }
3263
3295
  return response;
3264
3296
  }
package/dist/index.mjs CHANGED
@@ -264,6 +264,12 @@ var require_supports_color = __commonJS({
264
264
  }
265
265
  });
266
266
 
267
+ // src/plugin.ts
268
+ import {
269
+ EmailVerificationClaim
270
+ } from "supertokens-node/recipe/emailverification";
271
+ import Session3 from "supertokens-node/recipe/session";
272
+
267
273
  // ../../shared/js/src/createPluginInit.ts
268
274
  var import_supertokens_js_override = __toESM(require_build());
269
275
  var createPluginInitFunction = (init2, getImplementation, getNormalisedConfig = (config2) => config2) => {
@@ -610,11 +616,12 @@ function createLoggerConfig() {
610
616
  }
611
617
  var config = createLoggerConfig();
612
618
  var logger = common_default(config);
613
- if (logger.formatters) {
614
- logger.formatters.o = function(value) {
619
+ var formatters = logger.formatters;
620
+ if (formatters) {
621
+ formatters.o = function(value) {
615
622
  return util.inspect(value, { colors: this.useColors, compact: true }).split("\n").map((line) => line.trim()).join(" ");
616
623
  };
617
- logger.formatters.O = function(value) {
624
+ formatters.O = function(value) {
618
625
  return util.inspect(value, { colors: this.useColors, compact: false });
619
626
  };
620
627
  }
@@ -1845,12 +1852,12 @@ function hasVerifiedRealLoginMethod(user) {
1845
1852
  });
1846
1853
  }
1847
1854
  function getEffectiveAuthLevel(user, originalAuthLevel, verifiedData) {
1848
- if (originalAuthLevel === INSTANT_AUTH_METHOD_ID) {
1849
- return INSTANT_AUTH_METHOD_ID;
1850
- }
1851
1855
  if (hasVerifiedRealLoginMethod(user)) {
1852
1856
  return "verified";
1853
1857
  }
1858
+ if (originalAuthLevel === INSTANT_AUTH_METHOD_ID) {
1859
+ return INSTANT_AUTH_METHOD_ID;
1860
+ }
1854
1861
  return getGuestAuthLevel(user) || originalAuthLevel || (verifiedData && Object.keys(verifiedData).length > 0 ? "verified" : "unverified");
1855
1862
  }
1856
1863
  function canUpdateUserDataField(field) {
@@ -2279,6 +2286,7 @@ async function completePendingEmailVerification(input) {
2279
2286
  return;
2280
2287
  }
2281
2288
  let metadataUserId = userId;
2289
+ let verifiedRecipeUserId = input.recipeUserId;
2282
2290
  const passwordlessEmailMethod = getPasswordlessEmailLoginMethod(user);
2283
2291
  if (passwordlessEmailMethod) {
2284
2292
  const updateResult = await Passwordless.updateUser({
@@ -2291,6 +2299,7 @@ async function completePendingEmailVerification(input) {
2291
2299
  `Failed to update verified email method: ${updateResult.status}`
2292
2300
  );
2293
2301
  }
2302
+ verifiedRecipeUserId = passwordlessEmailMethod.recipeUserId;
2294
2303
  } else if (hasOnlyGuestLoginMethods(user)) {
2295
2304
  const isPasswordlessSignUpAllowed = await AccountLinking.isSignUpAllowed(
2296
2305
  PUBLIC_TENANT_ID,
@@ -2310,6 +2319,7 @@ async function completePendingEmailVerification(input) {
2310
2319
  tenantId: PUBLIC_TENANT_ID,
2311
2320
  userContext: input.userContext
2312
2321
  });
2322
+ verifiedRecipeUserId = passwordlessUser.recipeUserId;
2313
2323
  const primaryUserResult = await AccountLinking.createPrimaryUser(
2314
2324
  passwordlessUser.recipeUserId,
2315
2325
  input.userContext
@@ -2355,6 +2365,10 @@ async function completePendingEmailVerification(input) {
2355
2365
  )
2356
2366
  };
2357
2367
  await UserMetadata3.updateUserMetadata(metadataUserId, updatedMetadata);
2368
+ return {
2369
+ userId: metadataUserId,
2370
+ recipeUserId: verifiedRecipeUserId
2371
+ };
2358
2372
  }
2359
2373
  function isMatchingPendingEmailVerification(verification, email) {
2360
2374
  return verification.field === "email" && verification.value === email;
@@ -2783,6 +2797,12 @@ function validateWritableFields(fields) {
2783
2797
  }
2784
2798
 
2785
2799
  // src/plugin.ts
2800
+ var verifyRowndUserSessionOptions = {
2801
+ sessionRequired: true,
2802
+ overrideGlobalClaimValidators: (validators) => validators.filter((validator) => {
2803
+ return !("claim" in validator) || validator.claim.key !== EmailVerificationClaim.key;
2804
+ })
2805
+ };
2786
2806
  var init = createPluginInitFunction(
2787
2807
  (pluginConfig2) => {
2788
2808
  const rowndClient2 = createInstance({
@@ -2898,13 +2918,13 @@ var init = createPluginInitFunction(
2898
2918
  {
2899
2919
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user`,
2900
2920
  method: "get",
2901
- verifySessionOptions: { sessionRequired: true },
2921
+ verifySessionOptions: verifyRowndUserSessionOptions,
2902
2922
  handler: withRequestHandler(handleGetUser())
2903
2923
  },
2904
2924
  {
2905
2925
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user`,
2906
2926
  method: "put",
2907
- verifySessionOptions: { sessionRequired: true },
2927
+ verifySessionOptions: verifyRowndUserSessionOptions,
2908
2928
  handler: withRequestHandler(handleUpdateUser())
2909
2929
  },
2910
2930
  {
@@ -2928,13 +2948,13 @@ var init = createPluginInitFunction(
2928
2948
  {
2929
2949
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user/field`,
2930
2950
  method: "get",
2931
- verifySessionOptions: { sessionRequired: true },
2951
+ verifySessionOptions: verifyRowndUserSessionOptions,
2932
2952
  handler: withRequestHandler(handleGetUserField())
2933
2953
  },
2934
2954
  {
2935
2955
  path: `${apiBasePath}${HANDLE_BASE_PATH}/user/field`,
2936
2956
  method: "put",
2937
- verifySessionOptions: { sessionRequired: true },
2957
+ verifySessionOptions: verifyRowndUserSessionOptions,
2938
2958
  handler: withRequestHandler(handleUpdateUserField())
2939
2959
  }
2940
2960
  ]
@@ -3229,11 +3249,25 @@ var init = createPluginInitFunction(
3229
3249
  }
3230
3250
  const response = await originalImplementation.verifyEmailPOST(input);
3231
3251
  if (response.status === "OK") {
3232
- await completePendingEmailVerification({
3252
+ const verificationResult = await completePendingEmailVerification({
3233
3253
  recipeUserId: response.user.recipeUserId,
3234
3254
  email: response.user.email,
3235
3255
  userContext: input.userContext
3236
3256
  });
3257
+ const session = input.session;
3258
+ const shouldReplaceSession = session && verificationResult && (session.getUserId(input.userContext) !== verificationResult.userId || session.getRecipeUserId(input.userContext).getAsString() !== verificationResult.recipeUserId.getAsString());
3259
+ if (shouldReplaceSession) {
3260
+ await session.revokeSession(input.userContext);
3261
+ response.newSession = await Session3.createNewSession(
3262
+ input.options.req,
3263
+ input.options.res,
3264
+ session.getTenantId(input.userContext),
3265
+ verificationResult.recipeUserId,
3266
+ {},
3267
+ {},
3268
+ input.userContext
3269
+ );
3270
+ }
3237
3271
  }
3238
3272
  return response;
3239
3273
  }
package/package.json CHANGED
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "@supertokens-plugins/rownd-nodejs",
3
- "version": "0.3.0-beta.7",
3
+ "version": "0.5.0",
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": [