@supertokens-plugins/rownd-nodejs 0.2.1 → 0.3.0-beta.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/src/plugin.ts DELETED
@@ -1,189 +0,0 @@
1
- import { SuperTokensPlugin } from "supertokens-node/types";
2
- import { createPluginInitFunction } from "@shared/js";
3
- import { withRequestHandler } from "@shared/nodejs";
4
- import { createInstance } from "@rownd/node";
5
- import supertokens from "supertokens-node";
6
- import {
7
- PLUGIN_ID,
8
- PLUGIN_SDK_VERSION,
9
- HANDLE_BASE_PATH,
10
- PUBLIC_TENANT_ID,
11
- } from "./constants";
12
- import { RowndPluginConfig, RowndPluginNormalisedConfig } from "./types";
13
- import { enableDebugLogs, logDebugMessage } from "./logger";
14
- import Session from "supertokens-node/recipe/session";
15
- import { createClient } from "./telemetry/createTelemetryClient";
16
- import { RowndPluginError } from "./errors";
17
- import {
18
- parseRequest,
19
- mapRowndUserToSuperTokens,
20
- importUser,
21
- setRowndClient,
22
- validateRowndToken,
23
- fetchRowndUserInfo,
24
- } from "./pluginImplementation";
25
-
26
- export const init = createPluginInitFunction<
27
- SuperTokensPlugin,
28
- RowndPluginConfig,
29
- {},
30
- RowndPluginNormalisedConfig
31
- >(
32
- (config) => {
33
- const rowndClient = createInstance({
34
- app_key: config.rowndAppKey,
35
- app_secret: config.rowndAppSecret,
36
- });
37
- const telemetryClient = createClient(config.telemetry);
38
-
39
- setRowndClient(rowndClient);
40
-
41
- if (config.enableDebugLogs) {
42
- enableDebugLogs();
43
- }
44
-
45
- logDebugMessage("Rownd plugin init complete");
46
-
47
- return {
48
- id: PLUGIN_ID,
49
- compatibleSDKVersions: PLUGIN_SDK_VERSION,
50
- init: async () => {
51
- if (!supertokens.isRecipeInitialized("session")) {
52
- console.warn(
53
- "RowndMigrationPlugin: Session recipe is not initialized. Session migration will fail.",
54
- );
55
- }
56
- },
57
- routeHandlers(config) {
58
- const apiBasePath = config.appInfo.apiBasePath.getAsStringDangerous();
59
- return {
60
- status: "OK",
61
- routeHandlers: [
62
- {
63
- path: `${apiBasePath}${HANDLE_BASE_PATH}/migrate`,
64
- method: "post",
65
- handler: withRequestHandler(
66
- async (req, res, _session, userContext) => {
67
- const startedAt = Date.now();
68
- let tenantId: string | undefined = PUBLIC_TENANT_ID;
69
- let rowndUserId: string | undefined;
70
- let superTokensUserId: string | undefined;
71
- let user: Awaited<ReturnType<typeof supertokens.getUser>>;
72
- let recipeUserId:
73
- | Parameters<typeof Session.createNewSession>[3]
74
- | undefined;
75
- try {
76
- if (!config.supertokens) {
77
- throw new Error("Supertokens config not found");
78
- }
79
- const parsed = await parseRequest(req);
80
- rowndUserId = await validateRowndToken(parsed.token);
81
- user = await supertokens.getUser(rowndUserId, userContext);
82
-
83
- if (!user) {
84
- const rowndUser = await fetchRowndUserInfo(rowndUserId);
85
- const stUserImport = mapRowndUserToSuperTokens(rowndUser);
86
- const importedUser = await importUser(
87
- stUserImport,
88
- config.supertokens,
89
- );
90
- superTokensUserId = importedUser.id;
91
- if (importedUser.loginMethods[0]?.recipeUserId) {
92
- recipeUserId = supertokens.convertToRecipeUserId(
93
- importedUser.loginMethods[0].recipeUserId,
94
- );
95
- }
96
- logDebugMessage(
97
- `User migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`,
98
- );
99
- } else {
100
- superTokensUserId = user.id;
101
- recipeUserId = user.loginMethods[0]?.recipeUserId;
102
- logDebugMessage(
103
- `User already migrated. tenantId: ${PUBLIC_TENANT_ID}, rowndUserId: ${rowndUserId}`,
104
- );
105
- }
106
-
107
- if (!recipeUserId) {
108
- throw new Error("User not found or has no login methods");
109
- }
110
-
111
- await Session.createNewSession(
112
- req,
113
- res,
114
- PUBLIC_TENANT_ID,
115
- recipeUserId,
116
- {},
117
- {},
118
- userContext,
119
- );
120
-
121
- logDebugMessage(
122
- `Session migrated successfully. tenantId: ${PUBLIC_TENANT_ID}, userId: ${superTokensUserId}`,
123
- );
124
-
125
- telemetryClient.recordSuccess({
126
- outcome: "success",
127
- durationMs: Date.now() - startedAt,
128
- tenantId,
129
- rowndUserId,
130
- superTokensUserId,
131
- });
132
-
133
- return { status: "OK" };
134
- } catch (error) {
135
- logDebugMessage(
136
- `Migration failed. Error: ${
137
- error instanceof Error ? error.message : "Unknown error"
138
- }`,
139
- );
140
- telemetryClient.recordError({
141
- error,
142
- startedAt,
143
- tenantId,
144
- rowndUserId,
145
- superTokensUserId,
146
- });
147
- return {
148
- status: "ERROR",
149
- message:
150
- error instanceof RowndPluginError
151
- ? error.message
152
- : "Migration failed",
153
- };
154
- }
155
- },
156
- ),
157
- },
158
- ],
159
- };
160
- },
161
- };
162
- },
163
- () => ({}),
164
- (config: RowndPluginConfig) => {
165
- if (!config?.rowndAppKey || !config?.rowndAppSecret) {
166
- throw new Error("Missing rowndAppKey or rowndAppSecret in plugin config");
167
- }
168
- if (config.telemetry?.provider === "axiom") {
169
- if (!config.telemetry.token || !config.telemetry.dataset) {
170
- throw new Error(
171
- "Missing telemetry axiom token or dataset in plugin config",
172
- );
173
- }
174
- }
175
- if (config.telemetry?.provider === "custom") {
176
- if (typeof config.telemetry.factory !== "function") {
177
- throw new Error(
178
- "Missing telemetry custom factory function in plugin config",
179
- );
180
- }
181
- }
182
- return {
183
- rowndAppKey: config.rowndAppKey,
184
- rowndAppSecret: config.rowndAppSecret,
185
- enableDebugLogs: config.enableDebugLogs,
186
- telemetry: config.telemetry,
187
- };
188
- },
189
- );
@@ -1,187 +0,0 @@
1
- import { BaseRequest } from "supertokens-node/lib/build/framework/request";
2
- import { RowndUser, SuperTokensUserImport, IRowndClient } from "./types";
3
- import { RowndPluginError } from "./errors";
4
- import type {
5
- JSONObject,
6
- SuperTokensPublicConfig,
7
- } from "supertokens-node/types";
8
-
9
- let rowndClient: IRowndClient | undefined;
10
-
11
- export function setRowndClient(client: IRowndClient) {
12
- rowndClient = client;
13
- }
14
-
15
- export function getRowndClient() {
16
- if (!rowndClient) {
17
- throw new Error("Rownd client not initialized");
18
- }
19
- return rowndClient;
20
- }
21
-
22
- export async function parseRequest(req: BaseRequest): Promise<{
23
- token: string;
24
- }> {
25
- const authHeader = req.getHeaderValue("authorization");
26
- if (!authHeader) {
27
- throw new RowndPluginError("MISSING_AUTHORIZATION_HEADER");
28
- }
29
-
30
- const token = authHeader.replace(/^Bearer /i, "");
31
- if (!token) {
32
- throw new RowndPluginError("INVALID_TOKEN");
33
- }
34
-
35
- return {
36
- token,
37
- };
38
- }
39
-
40
- export function mapRowndUserToSuperTokens(
41
- rowndUser: RowndUser,
42
- ): SuperTokensUserImport {
43
- const loginMethods: SuperTokensUserImport["loginMethods"] = [];
44
- const rowndUserData = rowndUser.data || {};
45
- const rowndUserVerifiedData = rowndUser.verified_data || {};
46
- if (!rowndUserData.user_id) {
47
- throw new Error("Rownd user has no user_id");
48
- }
49
-
50
- if (rowndUserData.google_id) {
51
- if (!rowndUserData.email) {
52
- throw new Error("Rownd Google user is missing email");
53
- }
54
-
55
- loginMethods.push({
56
- recipeId: "thirdparty",
57
- thirdPartyId: "google",
58
- thirdPartyUserId: rowndUserData.google_id,
59
- email: rowndUserData.email,
60
- isVerified: !!rowndUserVerifiedData.google_id,
61
- });
62
- }
63
-
64
- if (rowndUserData.apple_id) {
65
- if (!rowndUserData.email) {
66
- throw new Error("Rownd Apple user is missing email");
67
- }
68
-
69
- loginMethods.push({
70
- recipeId: "thirdparty",
71
- thirdPartyId: "apple",
72
- thirdPartyUserId: rowndUserData.apple_id,
73
- email: rowndUserData.email,
74
- isVerified: !!rowndUserVerifiedData.apple_id,
75
- });
76
- }
77
-
78
- if (rowndUserData.phone_number) {
79
- loginMethods.push({
80
- recipeId: "passwordless",
81
- phoneNumber: rowndUserData.phone_number,
82
- isVerified: !!rowndUserVerifiedData.phone_number,
83
- });
84
- }
85
-
86
- if (rowndUserData.email && loginMethods.length === 0) {
87
- loginMethods.push({
88
- recipeId: "passwordless",
89
- email: rowndUserData.email,
90
- isVerified: !!rowndUserVerifiedData.email,
91
- });
92
- }
93
-
94
- if (loginMethods.length === 0) {
95
- throw new Error("No valid login methods found in Rownd user data");
96
- }
97
-
98
- const rowndUserMeta = rowndUser.meta || {};
99
- const rowndUserAttributes = rowndUser.attributes || {};
100
-
101
- const userMetadata: JSONObject = {
102
- data: {
103
- ...rowndUserData,
104
- },
105
- meta: {
106
- ...rowndUserMeta,
107
- },
108
- verified_data: {
109
- ...rowndUserVerifiedData,
110
- },
111
- attributes: {
112
- ...rowndUserAttributes,
113
- },
114
- rownd_migrated: true,
115
- rownd_user_id: rowndUserData.user_id,
116
- };
117
-
118
- return {
119
- externalUserId: rowndUserData.user_id,
120
- loginMethods,
121
- userMetadata,
122
- };
123
- }
124
-
125
- export async function importUser(
126
- stUser: SuperTokensUserImport,
127
- config: NonNullable<SuperTokensPublicConfig["supertokens"]>,
128
- ): Promise<{
129
- id: string;
130
- loginMethods: Array<{
131
- recipeUserId: string;
132
- }>;
133
- }> {
134
- const headers: Record<string, string> = {
135
- "Content-Type": "application/json",
136
- };
137
- if (config.apiKey) {
138
- headers["api-key"] = config.apiKey;
139
- }
140
-
141
- const response = await fetch(`${config.connectionURI}/bulk-import/import`, {
142
- method: "POST",
143
- headers,
144
- body: JSON.stringify(stUser),
145
- });
146
-
147
- if (!response.ok) {
148
- const errorText = await response.text();
149
- throw new Error(
150
- `Bulk import failed with status ${response.status}: ${errorText}`,
151
- );
152
- }
153
-
154
- const importResponse = (await response.json()) as {
155
- status: string;
156
- message?: string;
157
- user?: {
158
- id: string;
159
- loginMethods: Array<{
160
- recipeUserId: string;
161
- }>;
162
- };
163
- };
164
-
165
- if (importResponse.status !== "OK" || !importResponse.user) {
166
- throw new Error(
167
- `Bulk import failed: ${importResponse.message || "Missing user in response"}`,
168
- );
169
- }
170
-
171
- return importResponse.user;
172
- }
173
-
174
- export async function validateRowndToken(token: string): Promise<string> {
175
- const client = getRowndClient();
176
- const tokenInfo = await client.validateToken(token);
177
- return tokenInfo.user_id;
178
- }
179
-
180
- export async function fetchRowndUserInfo(userId: string): Promise<RowndUser> {
181
- const client = getRowndClient();
182
- const rowndUser = await client.fetchUserInfo({ user_id: userId });
183
- if (!rowndUser) {
184
- throw new RowndPluginError("ROWND_USER_NOT_FOUND");
185
- }
186
- return rowndUser;
187
- }
@@ -1,34 +0,0 @@
1
- import { PLUGIN_ID } from "../constants";
2
- import { RowndTelemetryClient, RowndTelemetryEvent } from "../types";
3
-
4
- const DEFAULT_AXIOM_URL = "https://api.axiom.co/v1/datasets";
5
-
6
- export class AxiomTelemetryClient implements RowndTelemetryClient {
7
- private readonly url: string;
8
-
9
- constructor(
10
- private readonly token: string,
11
- private readonly dataset: string,
12
- endpoint?: string,
13
- ) {
14
- const baseUrl = (endpoint ?? DEFAULT_AXIOM_URL).replace(/\/$/, "");
15
- this.url = `${baseUrl}/${dataset}/ingest`;
16
- }
17
-
18
- async recordEvent(event: RowndTelemetryEvent): Promise<void> {
19
- await fetch(this.url, {
20
- method: "POST",
21
- headers: {
22
- Authorization: `Bearer ${this.token}`,
23
- "Content-Type": "application/json",
24
- },
25
- body: JSON.stringify([
26
- {
27
- ts: new Date().toISOString(),
28
- plugin: PLUGIN_ID,
29
- ...event,
30
- },
31
- ]),
32
- });
33
- }
34
- }
@@ -1,87 +0,0 @@
1
- import {
2
- RowndTelemetryClient,
3
- RowndTelemetryConfig,
4
- RowndTelemetryEvent,
5
- } from "../types";
6
- import { logDebugMessage } from "../logger";
7
- import { AxiomTelemetryClient } from "./axiomTelemetryClient";
8
- import { OpenTelemetryClient } from "./openTelemetryClient";
9
-
10
- type SuccessEvent = Extract<RowndTelemetryEvent, { outcome: "success" }>;
11
- type ErrorEvent = Extract<RowndTelemetryEvent, { outcome: "error" }>;
12
-
13
- export function createClient(
14
- telemetryConfig: RowndTelemetryConfig | undefined,
15
- ) {
16
- let client: RowndTelemetryClient | undefined;
17
-
18
- if (telemetryConfig?.provider === "opentelemetry") {
19
- client = new OpenTelemetryClient();
20
- } else if (telemetryConfig?.provider === "axiom") {
21
- client = new AxiomTelemetryClient(
22
- telemetryConfig.token,
23
- telemetryConfig.dataset,
24
- telemetryConfig.url,
25
- );
26
- } else if (telemetryConfig?.provider === "custom") {
27
- client = telemetryConfig.factory();
28
- }
29
-
30
- return {
31
- recordSuccess: (event: SuccessEvent) => {
32
- if (!client) {
33
- return;
34
- }
35
- safeRecordEvent(client, event, "success");
36
- },
37
- recordError: (input: {
38
- error: unknown;
39
- startedAt: number;
40
- tenantId?: string;
41
- rowndUserId?: string;
42
- superTokensUserId?: string;
43
- }) => {
44
- if (!client) {
45
- return;
46
- }
47
-
48
- const event: ErrorEvent = {
49
- outcome: "error",
50
- durationMs: Date.now() - input.startedAt,
51
- tenantId: input.tenantId,
52
- rowndUserId: input.rowndUserId,
53
- superTokensUserId: input.superTokensUserId,
54
- error: {
55
- message:
56
- input.error instanceof Error
57
- ? input.error.message
58
- : "Unknown error",
59
- name: input.error instanceof Error ? input.error.name : undefined,
60
- },
61
- };
62
- safeRecordEvent(client, event, "error");
63
- },
64
- };
65
- }
66
-
67
- function safeRecordEvent(
68
- client: RowndTelemetryClient,
69
- event: RowndTelemetryEvent,
70
- outcome: "success" | "error",
71
- ) {
72
- try {
73
- Promise.resolve(client.recordEvent(event)).catch((error) => {
74
- logDebugMessage(
75
- `Failed to record telemetry ${outcome} event. Error: ${
76
- error instanceof Error ? error.message : "Unknown error"
77
- }`,
78
- );
79
- });
80
- } catch (error) {
81
- logDebugMessage(
82
- `Failed to record telemetry ${outcome} event. Error: ${
83
- error instanceof Error ? error.message : "Unknown error"
84
- }`,
85
- );
86
- }
87
- }
@@ -1,32 +0,0 @@
1
- import { SpanStatusCode, trace } from "@opentelemetry/api";
2
- import { RowndTelemetryClient, RowndTelemetryEvent } from "../types";
3
-
4
- export class OpenTelemetryClient implements RowndTelemetryClient {
5
- recordEvent(event: RowndTelemetryEvent): void {
6
- const tracer = trace.getTracer("supertokens-plugin-rownd", "0.1.0");
7
- const span = tracer.startSpan("rownd.migrate");
8
- span.setAttributes({
9
- "rownd.outcome": event.outcome,
10
- "rownd.duration_ms": event.durationMs,
11
- "rownd.tenant_id": event.tenantId ?? "",
12
- "rownd.rownd_user_id": event.rowndUserId ?? "",
13
- "rownd.supertokens_user_id": event.superTokensUserId ?? "",
14
- });
15
-
16
- if (event.outcome === "error") {
17
- const err = new Error(event.error.message);
18
- if (event.error.name) {
19
- err.name = event.error.name;
20
- }
21
- span.recordException(err);
22
- span.setStatus({
23
- code: SpanStatusCode.ERROR,
24
- message: event.error.message,
25
- });
26
- } else {
27
- span.setStatus({ code: SpanStatusCode.OK });
28
- }
29
-
30
- span.end();
31
- }
32
- }
package/src/types.ts DELETED
@@ -1,124 +0,0 @@
1
- import type { JSONObject } from "supertokens-node/types";
2
-
3
- export interface RowndPluginConfig {
4
- rowndAppKey: string;
5
- rowndAppSecret: string;
6
- enableDebugLogs?: boolean;
7
- telemetry?: RowndTelemetryConfig;
8
- }
9
-
10
- export type RowndTelemetryEvent =
11
- | {
12
- outcome: "success";
13
- durationMs: number;
14
- tenantId?: string;
15
- rowndUserId?: string;
16
- superTokensUserId?: string;
17
- }
18
- | {
19
- outcome: "error";
20
- durationMs: number;
21
- tenantId?: string;
22
- rowndUserId?: string;
23
- superTokensUserId?: string;
24
- error: {
25
- message: string;
26
- name?: string;
27
- };
28
- };
29
-
30
- export interface RowndTelemetryClient {
31
- recordEvent: (event: RowndTelemetryEvent) => Promise<void> | void;
32
- }
33
-
34
- export type RowndTelemetryConfig =
35
- | {
36
- provider: "opentelemetry";
37
- }
38
- | {
39
- provider: "axiom";
40
- token: string;
41
- dataset: string;
42
- url?: string;
43
- }
44
- | {
45
- provider: "custom";
46
- factory: () => RowndTelemetryClient;
47
- };
48
-
49
- export interface RowndUser {
50
- state: string;
51
- auth_level: string;
52
- data: {
53
- user_id: string;
54
- email?: string;
55
- phone_number?: string;
56
- google_id?: string;
57
- apple_id?: string;
58
- first_name?: string;
59
- last_name?: string;
60
- };
61
- attributes?: Record<string, string>;
62
- verified_data: {
63
- email?: string;
64
- phone_number?: string;
65
- google_id?: string;
66
- apple_id?: string;
67
- };
68
- groups?: string[];
69
- meta?: {
70
- created?: string;
71
- modified?: string;
72
- first_sign_in?: string;
73
- last_sign_in?: string;
74
- };
75
- }
76
-
77
- export interface MigrationResponse {
78
- status: "OK" | "ERROR";
79
- message?: string;
80
- }
81
-
82
- export interface RowndPluginNormalisedConfig {
83
- rowndAppKey: string;
84
- rowndAppSecret: string;
85
- enableDebugLogs?: boolean;
86
- telemetry?: RowndTelemetryConfig;
87
- }
88
-
89
- export interface SuperTokensUserImport {
90
- externalUserId: string;
91
- timeJoined?: number;
92
- userMetadata: JSONObject;
93
- loginMethods: (
94
- | {
95
- recipeId: "emailpassword";
96
- email: string;
97
- passwordHash: string;
98
- isVerified: boolean;
99
- }
100
- | {
101
- recipeId: "thirdparty";
102
- thirdPartyId: string;
103
- thirdPartyUserId: string;
104
- email: string;
105
- isVerified: boolean;
106
- }
107
- | {
108
- recipeId: "passwordless";
109
- email?: string;
110
- phoneNumber?: string;
111
- isVerified: boolean;
112
- }
113
- )[];
114
- }
115
-
116
- export interface IRowndClient {
117
- validateToken: (token: string) => Promise<{
118
- user_id: string;
119
- }>;
120
- fetchUserInfo: (opts: {
121
- user_id: string;
122
- app_id?: string;
123
- }) => Promise<RowndUser | undefined>;
124
- }