@supertokens-plugins/rownd-nodejs 0.2.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 ADDED
@@ -0,0 +1,164 @@
1
+ # SuperTokens Rownd User Migration Plugin
2
+
3
+ This plugin facilitates the migration of users and sessions from Rownd to SuperTokens.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @supertokens-plugins/rownd-nodejs
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Backend Configuration
14
+
15
+ Initialize the plugin in your SuperTokens backend configuration.
16
+
17
+ > [!IMPORTANT]
18
+ > This plugin requires the `Session` and `UserMetadata` recipes to be initialized in your SuperTokens configuration.
19
+
20
+ ```typescript
21
+ import SuperTokens from "supertokens-node";
22
+ import Session from "supertokens-node/recipe/session";
23
+ import UserMetadata from "supertokens-node/recipe/usermetadata";
24
+ import RowndMigrationPlugin from "@supertokens-plugins/rownd-nodejs";
25
+
26
+ SuperTokens.init({
27
+ appInfo: {
28
+ // your app info
29
+ },
30
+ recipeList: [
31
+ Session.init(),
32
+ UserMetadata.init(),
33
+ // your other recipes
34
+ ],
35
+ experimental: {
36
+ plugins: [
37
+ RowndMigrationPlugin.init({
38
+ rowndAppKey: process.env.ROWND_APP_KEY,
39
+ rowndAppSecret: process.env.ROWND_APP_SECRET,
40
+ enableDebugLogs: process.env.ENABLE_DEBUG_LOGS === "true",
41
+ }),
42
+ ],
43
+ },
44
+ });
45
+ ```
46
+
47
+ ## API Endpoint
48
+
49
+ The plugin exposes a single endpoint:
50
+
51
+ > [!IMPORTANT]
52
+ > The plugin always migrates users and sessions into the `public` tenant.
53
+ > Rownd users with multiple supported login methods are rejected unless SuperTokens account linking is enabled in the target environment.
54
+
55
+ ### Migrate
56
+
57
+ - **POST** `/plugin/rownd/migrate`
58
+ - **Headers**: `Authorization: Bearer <Rownd_JWT>`
59
+ - **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.
60
+
61
+ ## Debug Logging
62
+
63
+ Set `enableDebugLogs: true` in the plugin config to enable debug logging.
64
+
65
+ ## Telemetry
66
+
67
+ Telemetry is optional. If `telemetry` is omitted from the plugin config, no telemetry is emitted.
68
+
69
+ The plugin emits exactly one telemetry event per `/migrate` call result.
70
+
71
+ ### Event shape
72
+
73
+ Each event includes endpoint outcome data only (not step-by-step events), including:
74
+
75
+ - `operation`: `migrate`
76
+ - `outcome`: `success` or `error`
77
+ - `durationMs`
78
+ - `tenantId` (when available)
79
+ - `rowndUserId` (when available)
80
+ - `superTokensUserId` (when available)
81
+ - `migrationState`: `already-migrated` or `imported-during-request` (when available)
82
+ - for errors: `error.message` and `error.name`
83
+
84
+ > [!NOTE]
85
+ > Telemetry failures never fail migration endpoints. Errors in telemetry reporting are swallowed.
86
+
87
+ ### Provider: OpenTelemetry
88
+
89
+ ```typescript
90
+ RowndMigrationPlugin.init({
91
+ rowndAppKey: process.env.ROWND_APP_KEY,
92
+ rowndAppSecret: process.env.ROWND_APP_SECRET,
93
+ telemetry: {
94
+ provider: "opentelemetry",
95
+ },
96
+ });
97
+ ```
98
+
99
+ > [!IMPORTANT]
100
+ > This plugin uses `@opentelemetry/api` only. You still need to initialize OpenTelemetry SDK/exporters in your app for spans to be exported.
101
+
102
+ ### Provider: Axiom
103
+
104
+ ```typescript
105
+ RowndMigrationPlugin.init({
106
+ rowndAppKey: process.env.ROWND_APP_KEY,
107
+ rowndAppSecret: process.env.ROWND_APP_SECRET,
108
+ telemetry: {
109
+ provider: "axiom",
110
+ token: process.env.AXIOM_TOKEN!,
111
+ dataset: process.env.AXIOM_DATASET!,
112
+ // optional, defaults to https://api.axiom.co/v1/datasets
113
+ // url: "https://api.axiom.co/v1/datasets",
114
+ },
115
+ });
116
+ ```
117
+
118
+ ### Provider: Custom
119
+
120
+ ```typescript
121
+ RowndMigrationPlugin.init({
122
+ rowndAppKey: process.env.ROWND_APP_KEY,
123
+ rowndAppSecret: process.env.ROWND_APP_SECRET,
124
+ telemetry: {
125
+ provider: "custom",
126
+ factory: () => ({
127
+ recordEvent: async (event) => {
128
+ // send to your telemetry backend
129
+ },
130
+ }),
131
+ },
132
+ });
133
+ ```
134
+
135
+ ## Bulk Import Script
136
+
137
+ The package includes a bulk migration script for importing Rownd users into SuperTokens.
138
+
139
+ The script now runs directly from a YAML config file that lives beside the script:
140
+
141
+ - config file: `packages/rownd-nodejs/scripts/config.yaml`
142
+ - script: `packages/rownd-nodejs/scripts/bulkMigrate.ts`
143
+
144
+ ### Usage
145
+
146
+ 1. Edit `scripts/config.yaml` with your Rownd and SuperTokens credentials.
147
+ 2. Run the script from `packages/rownd-nodejs`.
148
+
149
+ ```bash
150
+ npm run bulk-import
151
+ ```
152
+
153
+ The script:
154
+
155
+ - fetches users from Rownd page by page
156
+ - validates the Rownd payload shape with `zod`
157
+ - maps users with `mapRowndUserToSuperTokens`
158
+ - imports them into SuperTokens in bounded batches
159
+ - writes a checkpoint file so the run can resume later
160
+
161
+ ### Config File
162
+
163
+ All runtime config is read from `scripts/config.yaml`.
164
+ There is no environment variable parsing.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@supertokens-plugins/rownd-nodejs",
3
+ "version": "0.2.0",
4
+ "description": "Rownd User Migration Plugin for SuperTokens",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src",
11
+ "scripts"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format cjs,esm --dts",
15
+ "bulk-import": "tsx scripts/bulkMigrate.ts",
16
+ "lint": "eslint ./src --ext .ts",
17
+ "test": "TEST_MODE=testing vitest run --pool=forks"
18
+ },
19
+ "keywords": [
20
+ "supertokens",
21
+ "rownd",
22
+ "migration",
23
+ "plugin"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "dependencies": {
27
+ "@opentelemetry/api": "^1.9.0",
28
+ "@rownd/node": "^3.0.4"
29
+ },
30
+ "peerDependencies": {
31
+ "supertokens-node": ">=23.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@shared/eslint": "*",
35
+ "@shared/nodejs": "*",
36
+ "@shared/tsconfig": "*",
37
+ "@types/node": "^20.0.0",
38
+ "eslint": "^8.57.0",
39
+ "testcontainers": "^10.7.1",
40
+ "tsup": "^8.0.2",
41
+ "tsx": "^4.21.0",
42
+ "typescript": "^5.7.3",
43
+ "vitest": "^1.3.1",
44
+ "yaml": "^2.8.3",
45
+ "zod": "^4.3.6"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }
@@ -0,0 +1,450 @@
1
+ import fs from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { z } from "zod";
5
+
6
+ import { type RowndUser, type SuperTokensUserImport } from "../src/types";
7
+ import { mapRowndUserToSuperTokens } from "../src/pluginImplementation";
8
+
9
+ const DEFAULT_CONFIG_FILE_NAME = "config.yaml";
10
+ const SCRIPT_DIR = __dirname;
11
+ const DEFAULT_CONFIG_FILE_PATH = resolve(SCRIPT_DIR, DEFAULT_CONFIG_FILE_NAME);
12
+
13
+ export type RetryConfig = {
14
+ maxAttempts: number;
15
+ initialDelayMs: number;
16
+ };
17
+
18
+ export type CheckpointConfig = {
19
+ file: string;
20
+ resume: boolean;
21
+ };
22
+
23
+ export type RowndSourceConfig = {
24
+ appId: string;
25
+ appKey: string;
26
+ appSecret: string;
27
+ pageSize: number;
28
+ };
29
+
30
+ export type SuperTokensTargetConfig = {
31
+ connectionURI: string;
32
+ apiKey?: string;
33
+ batchSize: number;
34
+ };
35
+
36
+ export type BulkMigrateConfig = {
37
+ limit: number;
38
+ checkpoint: CheckpointConfig;
39
+ retry: RetryConfig;
40
+ rownd: RowndSourceConfig;
41
+ supertokens: SuperTokensTargetConfig;
42
+ };
43
+
44
+ type Checkpoint = {
45
+ cursor?: string;
46
+ importedCount?: number;
47
+ updatedAt: string;
48
+ };
49
+
50
+ const ConfigSchema = z.object({
51
+ limit: z.preprocess(
52
+ (value) => (value === undefined ? Number.POSITIVE_INFINITY : value),
53
+ z.union([z.literal(Number.POSITIVE_INFINITY), z.number().int().positive()]),
54
+ ),
55
+ checkpoint: z.object({
56
+ file: z.string(),
57
+ resume: z.boolean().default(false),
58
+ }),
59
+ retry: z.object({
60
+ maxAttempts: z.number().int().positive(),
61
+ initialDelayMs: z.number().int().positive(),
62
+ }),
63
+ rownd: z.object({
64
+ appId: z.string(),
65
+ appKey: z.string(),
66
+ appSecret: z.string(),
67
+ pageSize: z.number().int().positive(),
68
+ }),
69
+ supertokens: z.object({
70
+ connectionURI: z.string(),
71
+ apiKey: z.string().optional(),
72
+ batchSize: z.number().int().positive(),
73
+ }),
74
+ });
75
+
76
+ const RowndUserSchema = z.looseObject({
77
+ state: z.string().optional(),
78
+ auth_level: z.string().optional(),
79
+ data: z.looseObject({
80
+ user_id: z.string(),
81
+ email: z.string().optional(),
82
+ phone_number: z.string().optional(),
83
+ google_id: z.string().optional(),
84
+ apple_id: z.string().optional(),
85
+ first_name: z.string().optional(),
86
+ last_name: z.string().optional(),
87
+ }),
88
+ verified_data: z.record(z.string(), z.unknown()).optional(),
89
+ attributes: z.record(z.string(), z.unknown()).optional(),
90
+ groups: z.array(z.string()).optional(),
91
+ meta: z.record(z.string(), z.unknown()).optional(),
92
+ });
93
+
94
+ const RowndUsersPageSchema = z
95
+ .object({
96
+ results: z.array(RowndUserSchema).optional(),
97
+ data: z.array(RowndUserSchema).optional(),
98
+ })
99
+ .superRefine((value, context) => {
100
+ if (!value.results && !value.data) {
101
+ context.addIssue({
102
+ code: z.ZodIssueCode.custom,
103
+ message: "Rownd API response is missing a results/data array.",
104
+ });
105
+ }
106
+ });
107
+
108
+ const CheckpointSchema = z.object({
109
+ cursor: z.string().optional(),
110
+ importedCount: z.number().int().nonnegative().optional(),
111
+ updatedAt: z.string(),
112
+ });
113
+
114
+ function formatIssuePath(path: Array<string | number>) {
115
+ if (path.length === 0) {
116
+ return "<root>";
117
+ }
118
+
119
+ return path
120
+ .map((segment, index) => {
121
+ if (typeof segment === "number") {
122
+ return `[${segment}]`;
123
+ }
124
+
125
+ return index === 0 ? segment : `.${segment}`;
126
+ })
127
+ .join("");
128
+ }
129
+
130
+ export function formatZodError(error: z.ZodError) {
131
+ return error.issues
132
+ .map(
133
+ (issue) =>
134
+ `${formatIssuePath(issue.path.filter((segment): segment is string | number => typeof segment === "string" || typeof segment === "number"))}: ${issue.message}`,
135
+ )
136
+ .join("\n");
137
+ }
138
+
139
+ export function parseConfig(
140
+ rawConfig: unknown,
141
+ configDir: string = SCRIPT_DIR,
142
+ ): BulkMigrateConfig {
143
+ const parsed = ConfigSchema.parse(rawConfig);
144
+ const checkpointFile = isAbsolute(parsed.checkpoint.file)
145
+ ? parsed.checkpoint.file
146
+ : resolve(configDir, parsed.checkpoint.file);
147
+
148
+ return {
149
+ limit: parsed.limit,
150
+ checkpoint: {
151
+ file: checkpointFile,
152
+ resume: parsed.checkpoint.resume,
153
+ },
154
+ retry: parsed.retry,
155
+ rownd: parsed.rownd,
156
+ supertokens: parsed.supertokens,
157
+ };
158
+ }
159
+
160
+ export async function loadConfig(
161
+ configFilePath: string = DEFAULT_CONFIG_FILE_PATH,
162
+ ) {
163
+ const configFile = await fs.readFile(configFilePath, "utf8");
164
+
165
+ return parseConfig(parseYaml(configFile), dirname(configFilePath));
166
+ }
167
+
168
+ function isRetryableStatus(status: number) {
169
+ return status === 429 || status >= 500;
170
+ }
171
+
172
+ async function fetchWithRetry({
173
+ url,
174
+ requestInit,
175
+ retryConfig,
176
+ operation,
177
+ }: {
178
+ url: string;
179
+ requestInit?: RequestInit;
180
+ retryConfig: RetryConfig;
181
+ operation: string;
182
+ }) {
183
+ let lastError: Error | undefined;
184
+
185
+ for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt += 1) {
186
+ try {
187
+ const response = await fetch(url, requestInit);
188
+ if (
189
+ response.ok ||
190
+ !isRetryableStatus(response.status) ||
191
+ attempt === retryConfig.maxAttempts
192
+ ) {
193
+ return response;
194
+ }
195
+
196
+ const responseText = await response.text();
197
+ lastError = new Error(
198
+ `${operation} failed with ${response.status}${responseText ? ` ${responseText}` : ""}`,
199
+ );
200
+ } catch (error) {
201
+ if (attempt === retryConfig.maxAttempts) {
202
+ throw error;
203
+ }
204
+
205
+ lastError =
206
+ error instanceof Error ? error : new Error(`${operation} failed`);
207
+ }
208
+
209
+ const delayMs = Math.round(
210
+ retryConfig.initialDelayMs * 2 ** (attempt - 1) * (1 + Math.random() * 0.2),
211
+ );
212
+ console.log(
213
+ `${operation} attempt ${attempt} failed. Retrying in ${delayMs}ms.`,
214
+ );
215
+
216
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
217
+ }
218
+
219
+ throw lastError ?? new Error(`${operation} failed`);
220
+ }
221
+
222
+ function parseRowndUser(rawUser: unknown) {
223
+ const parsed = RowndUserSchema.parse(rawUser);
224
+ const rowndUserId = (parsed.data.user_id ||
225
+ parsed.user_id ||
226
+ parsed.app_user_id) as string | undefined;
227
+
228
+ if (!rowndUserId) {
229
+ throw new Error(
230
+ "Rownd user is missing a stable user id. Cannot continue pagination safely.",
231
+ );
232
+ }
233
+
234
+ const rowndUser: RowndUser = {
235
+ state: parsed.state ?? "",
236
+ auth_level: parsed.auth_level ?? "",
237
+ data: parsed.data as RowndUser["data"],
238
+ verified_data: (parsed.verified_data ?? {}) as RowndUser["verified_data"],
239
+ attributes: parsed.attributes as RowndUser["attributes"],
240
+ groups: parsed.groups,
241
+ meta: parsed.meta as RowndUser["meta"],
242
+ };
243
+
244
+ return { rowndUser, rowndUserId };
245
+ }
246
+
247
+ async function fetchRowndUsersPage(
248
+ config: BulkMigrateConfig,
249
+ cursor?: string,
250
+ pageSize?: number,
251
+ ) {
252
+ const url = new URL(
253
+ `/applications/${config.rownd.appId}/users/data`,
254
+ "https://api.rownd.io",
255
+ );
256
+
257
+ if (cursor) {
258
+ url.searchParams.set("after", cursor);
259
+ }
260
+
261
+ url.searchParams.set("include_duplicates", "true");
262
+ url.searchParams.set("page_size", String(pageSize ?? config.rownd.pageSize));
263
+
264
+ const response = await fetchWithRetry({
265
+ url: url.toString(),
266
+ requestInit: {
267
+ headers: {
268
+ "x-rownd-app-key": config.rownd.appKey,
269
+ "x-rownd-app-secret": config.rownd.appSecret,
270
+ },
271
+ },
272
+ retryConfig: config.retry,
273
+ operation: "Fetching Rownd users",
274
+ });
275
+
276
+ if (!response.ok) {
277
+ throw new Error(
278
+ `Rownd API error: ${response.status} ${await response.text()}`,
279
+ );
280
+ }
281
+
282
+ const page = RowndUsersPageSchema.parse(await response.json());
283
+ return (page.results ?? page.data ?? []).map(parseRowndUser);
284
+ }
285
+
286
+ async function loadCheckpoint(checkpointFile: string) {
287
+ try {
288
+ const checkpoint = await fs.readFile(checkpointFile, "utf8");
289
+ return CheckpointSchema.parse(JSON.parse(checkpoint));
290
+ } catch (error) {
291
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
292
+ return null;
293
+ }
294
+
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ async function saveCheckpoint(checkpointFile: string, checkpoint: Checkpoint) {
300
+ const tempFile = `${checkpointFile}.tmp`;
301
+ await fs.writeFile(tempFile, JSON.stringify(checkpoint, null, 2), "utf8");
302
+ await fs.rename(tempFile, checkpointFile);
303
+ }
304
+
305
+ export async function stageUsersForImport(config: {
306
+ users: SuperTokensUserImport[];
307
+ supertokens: Pick<SuperTokensTargetConfig, "connectionURI" | "apiKey">;
308
+ retry: RetryConfig;
309
+ }) {
310
+ const headers: Record<string, string> = {
311
+ "Content-Type": "application/json",
312
+ };
313
+
314
+ if (config.supertokens.apiKey) {
315
+ headers["api-key"] = config.supertokens.apiKey;
316
+ }
317
+
318
+ const response = await fetchWithRetry({
319
+ url: new URL(
320
+ "/bulk-import/users",
321
+ config.supertokens.connectionURI,
322
+ ).toString(),
323
+ requestInit: {
324
+ method: "POST",
325
+ headers,
326
+ body: JSON.stringify({ users: config.users }),
327
+ },
328
+ retryConfig: config.retry,
329
+ operation: "Importing users into SuperTokens",
330
+ });
331
+
332
+ if (!response.ok) {
333
+ throw new Error(
334
+ `SuperTokens bulk import error: ${response.status} ${await response.text()}`,
335
+ );
336
+ }
337
+ }
338
+
339
+ async function importUsersBatch(
340
+ config: BulkMigrateConfig,
341
+ users: Array<{ rowndUserId: string; user: SuperTokensUserImport }>,
342
+ totalImportedBeforeBatch: number,
343
+ ) {
344
+ for (
345
+ let index = 0;
346
+ index < users.length;
347
+ index += config.supertokens.batchSize
348
+ ) {
349
+ const batch = users.slice(index, index + config.supertokens.batchSize);
350
+ console.log(
351
+ `Importing batch ${Math.floor(totalImportedBeforeBatch / config.supertokens.batchSize) + 1} (${batch.length} users)`,
352
+ );
353
+ await stageUsersForImport({
354
+ users: batch.map((entry) => entry.user),
355
+ supertokens: config.supertokens,
356
+ retry: config.retry,
357
+ });
358
+ }
359
+ }
360
+
361
+ export async function migrateRowndUsersToSuperTokens(
362
+ config: BulkMigrateConfig,
363
+ ) {
364
+ const checkpoint = config.checkpoint.resume
365
+ ? await loadCheckpoint(config.checkpoint.file)
366
+ : null;
367
+ let cursor = checkpoint?.cursor;
368
+ let totalProcessed = 0;
369
+ let totalImported = checkpoint?.importedCount ?? 0;
370
+
371
+ if (config.checkpoint.resume) {
372
+ console.log(
373
+ checkpoint
374
+ ? `Resuming migration from cursor ${checkpoint.cursor ?? "<start>"}`
375
+ : "No checkpoint found. Starting a fresh migration.",
376
+ );
377
+ }
378
+
379
+ while (totalProcessed < config.limit) {
380
+ const remaining = Number.isFinite(config.limit)
381
+ ? Math.max(config.limit - totalProcessed, 0)
382
+ : config.rownd.pageSize;
383
+ const requestedPageSize = Number.isFinite(config.limit)
384
+ ? Math.min(config.rownd.pageSize, remaining)
385
+ : config.rownd.pageSize;
386
+
387
+ if (requestedPageSize === 0) {
388
+ break;
389
+ }
390
+
391
+ console.log(
392
+ `Fetching page ${Math.floor(totalProcessed / config.rownd.pageSize) + 1}`,
393
+ );
394
+ const pageUsers = await fetchRowndUsersPage(
395
+ config,
396
+ cursor,
397
+ requestedPageSize,
398
+ );
399
+
400
+ if (pageUsers.length === 0) {
401
+ break;
402
+ }
403
+
404
+ const mappedUsers = pageUsers.map(({ rowndUser, rowndUserId }) => ({
405
+ rowndUserId,
406
+ user: mapRowndUserToSuperTokens(rowndUser),
407
+ }));
408
+
409
+ await importUsersBatch(config, mappedUsers, totalImported);
410
+
411
+ totalProcessed += pageUsers.length;
412
+ totalImported += mappedUsers.length;
413
+ cursor = pageUsers[pageUsers.length - 1]?.rowndUserId;
414
+
415
+ await saveCheckpoint(config.checkpoint.file, {
416
+ cursor,
417
+ importedCount: totalImported,
418
+ updatedAt: new Date().toISOString(),
419
+ });
420
+
421
+ console.log(`Processed ${totalProcessed} users so far`);
422
+ console.log(`Imported ${totalImported} users so far`);
423
+
424
+ if (pageUsers.length < requestedPageSize) {
425
+ break;
426
+ }
427
+ }
428
+
429
+ return {
430
+ totalProcessed,
431
+ totalImported,
432
+ cursor,
433
+ };
434
+ }
435
+
436
+ export async function runCli() {
437
+ const result = await migrateRowndUsersToSuperTokens(await loadConfig());
438
+ console.log(`Migrated ${result.totalImported} users`);
439
+ }
440
+
441
+ runCli().catch((error: unknown) => {
442
+ if (error instanceof z.ZodError) {
443
+ console.error(formatZodError(error));
444
+ } else {
445
+ console.error(
446
+ error instanceof Error ? error.message : "Bulk migration failed",
447
+ );
448
+ }
449
+ process.exitCode = 1;
450
+ });
@@ -0,0 +1,27 @@
1
+ # Maximum number of users to process. Remove this field to process everything.
2
+ limit: 100
3
+
4
+ checkpoint:
5
+ # File used to persist migration progress so a later run can resume.
6
+ file: ./rownd-migration-checkpoint.json
7
+ # When true, continue from the saved checkpoint instead of starting over.
8
+ resume: false
9
+
10
+ retry:
11
+ # Number of attempts for retryable HTTP failures such as 429 and 5xx.
12
+ maxAttempts: 5
13
+ # Base backoff delay in milliseconds before exponential retry waits.
14
+ initialDelayMs: 500
15
+
16
+ rownd:
17
+ appId: <APP_ID>
18
+ appKey: <APP_KEY>
19
+ appSecret: <APP_SECRET>
20
+ # Number of Rownd users to request per page.
21
+ pageSize: 100
22
+
23
+ supertokens:
24
+ connectionURI: <CONNECTION_URI>
25
+ apiKey: <API_KEY>
26
+ # Number of users to send to SuperTokens per bulk import request.
27
+ batchSize: 500
@@ -0,0 +1,5 @@
1
+ export const PLUGIN_ID = "supertokens-plugin-rownd";
2
+
3
+ export const PLUGIN_SDK_VERSION = ["23.0.0", "23.0.1", ">=23.0.1"];
4
+ export const HANDLE_BASE_PATH = "/plugin/rownd";
5
+ export const PUBLIC_TENANT_ID = "public";
package/src/errors.ts ADDED
@@ -0,0 +1,13 @@
1
+ export const ROWND_PLUGIN_ERROR_MESSAGES = {
2
+ MISSING_AUTHORIZATION_HEADER: "Missing authorization header",
3
+ INVALID_TOKEN: "Invalid token",
4
+ ROWND_USER_NOT_FOUND: "User not found in Rownd",
5
+ } as const;
6
+
7
+ export type RowndPluginErrorType = keyof typeof ROWND_PLUGIN_ERROR_MESSAGES;
8
+
9
+ export class RowndPluginError extends Error {
10
+ constructor(type: RowndPluginErrorType) {
11
+ super(ROWND_PLUGIN_ERROR_MESSAGES[type]);
12
+ }
13
+ }