@tomo-inc/cubist-wallet-sdk 0.0.4

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.
@@ -0,0 +1,150 @@
1
+ import { userExportKeygen } from "./cube-libs";
2
+
3
+ const EXPORT_KEY_STORAGE_KEY = "cubist_export_key";
4
+
5
+ interface StoredKeyPair {
6
+ publicKey: JsonWebKey;
7
+ privateKey: JsonWebKey;
8
+ }
9
+
10
+ export class CubeExportKeyService {
11
+ private static instance: CubeExportKeyService;
12
+ public exportKey: CryptoKeyPair | null = null;
13
+
14
+ public constructor() {
15
+ if (!window?.localStorage) {
16
+ throw new Error("localStorage is not available");
17
+ }
18
+ this.ensureExportKey();
19
+ }
20
+
21
+ public static getInstance() {
22
+ if (!CubeExportKeyService.instance) {
23
+ CubeExportKeyService.instance = new CubeExportKeyService();
24
+ }
25
+ return CubeExportKeyService.instance;
26
+ }
27
+
28
+ /**
29
+ * Recover exportKey from localStorage
30
+ */
31
+ private async recoverExportKey(): Promise<void> {
32
+ if (typeof window === "undefined" || !window.localStorage) {
33
+ return;
34
+ }
35
+
36
+ try {
37
+ const storedKey = window.localStorage.getItem(EXPORT_KEY_STORAGE_KEY);
38
+ if (storedKey) {
39
+ const keyData: StoredKeyPair = JSON.parse(storedKey);
40
+
41
+ // Import public key from JWK
42
+ const publicKey = await crypto.subtle.importKey(
43
+ "jwk",
44
+ keyData.publicKey,
45
+ {
46
+ name: "ECDH",
47
+ namedCurve: "P-256",
48
+ },
49
+ true,
50
+ [],
51
+ );
52
+
53
+ // Import private key from JWK
54
+ const privateKey = await crypto.subtle.importKey(
55
+ "jwk",
56
+ keyData.privateKey,
57
+ {
58
+ name: "ECDH",
59
+ namedCurve: "P-256",
60
+ },
61
+ true,
62
+ ["deriveKey", "deriveBits"],
63
+ );
64
+
65
+ this.exportKey = { publicKey, privateKey };
66
+ }
67
+ } catch (error) {
68
+ console.error("Failed to recover exportKey from localStorage:", error);
69
+ // If recovery fails, clear corrupted data
70
+ this.clearStoredExportKey();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Generate new exportKey and store it in localStorage
76
+ */
77
+ private async generateAndStoreExportKey(): Promise<void> {
78
+ try {
79
+ const key = await userExportKeygen();
80
+ this.exportKey = key;
81
+ await this.storeExportKey(key);
82
+ } catch (error) {
83
+ console.error("Failed to generate exportKey:", error);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Store exportKey in localStorage by exporting to JWK format
89
+ */
90
+ private async storeExportKey(keyPair: CryptoKeyPair): Promise<void> {
91
+ if (typeof window === "undefined" || !window.localStorage) {
92
+ return;
93
+ }
94
+
95
+ try {
96
+ // Export public key to JWK format
97
+ const publicKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
98
+
99
+ // Export private key to JWK format
100
+ const privateKeyJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
101
+
102
+ // Store as JSON
103
+ const keyData: StoredKeyPair = {
104
+ publicKey: publicKeyJwk,
105
+ privateKey: privateKeyJwk,
106
+ };
107
+
108
+ window.localStorage.setItem(EXPORT_KEY_STORAGE_KEY, JSON.stringify(keyData));
109
+ } catch (error) {
110
+ console.error("Failed to store exportKey in localStorage:", error);
111
+ // Handle quota exceeded error
112
+ if (error instanceof DOMException && error.name === "QuotaExceededError") {
113
+ console.warn("localStorage quota exceeded, clearing old data");
114
+ // Optionally clear old data or handle gracefully
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Clear stored exportKey from localStorage
121
+ */
122
+ private clearStoredExportKey(): void {
123
+ if (typeof window === "undefined" || !window.localStorage) {
124
+ return;
125
+ }
126
+
127
+ try {
128
+ window.localStorage.removeItem(EXPORT_KEY_STORAGE_KEY);
129
+ } catch (error) {
130
+ console.error("Failed to clear exportKey from localStorage:", error);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Ensure exportKey is ready before use
136
+ */
137
+ private async ensureExportKey(): Promise<void> {
138
+ if (this.exportKey) {
139
+ return;
140
+ }
141
+
142
+ // Try to recover from localStorage
143
+ await this.recoverExportKey();
144
+
145
+ // If still not available, generate a new one
146
+ if (!this.exportKey) {
147
+ await this.generateAndStoreExportKey();
148
+ }
149
+ }
150
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { CubeConnect } from "./cube-connect";
2
+ import { CubeExportService } from "./cube-export";
3
+ import { CubeAccountService } from "./cube-account";
4
+ import { CubeMfaService } from "./cube-mfa";
5
+ import { CubeSignService } from "./cube-sign";
6
+
7
+ export * from "./const";
8
+
9
+ export { CubeConnect, CubeExportService, CubeAccountService, CubeMfaService, CubeSignService };
10
+
11
+ export * from "./types";
12
+
13
+ export type { SessionData } from "./cube-libs";
14
+ import { CubeConfig } from "./types";
15
+ export type { CubeConfig };
package/src/passkey.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { cache, CubeStage, RelayOrigins, TomoStage } from "@tomo-inc/wallet-utils";
2
+ import { UserToken } from "./types";
3
+
4
+ const RELAY_NAME = "passkey-relay";
5
+ const SENDER_NAME = "cubist-sdk";
6
+
7
+ let passkeyRelayWindow: any = null;
8
+ let windowCloseInterval: NodeJS.Timeout | null = null;
9
+
10
+ export interface CubeRelayConfig {
11
+ rpId: string;
12
+ tomoStage: TomoStage;
13
+ cubeStage?: CubeStage;
14
+ tomoClientId: string;
15
+ oidcToken?: string;
16
+ }
17
+
18
+ export const openWindow = ({ url, name, width, height }: any): Window | null => {
19
+ const RN = (window as any).ReactNativeWebView;
20
+ if (RN) {
21
+ const message = {
22
+ url,
23
+ name,
24
+ };
25
+ RN.postMessage(
26
+ JSON.stringify({
27
+ type: "OPEN_PASSKEY_RELAY",
28
+ message,
29
+ }),
30
+ );
31
+
32
+ return {
33
+ closed: false,
34
+ postMessage: (message: any) => {
35
+ RN.postMessage(
36
+ JSON.stringify({
37
+ type: "PASSKEY_RELAY_MESSAGE",
38
+ message,
39
+ }),
40
+ );
41
+ },
42
+ } as any;
43
+ }
44
+
45
+ const top = (window.innerHeight - (height || 400)) / 2 + window.screenY;
46
+ const left = (window.innerWidth - (width || 400)) / 2 + window.screenX;
47
+
48
+ try {
49
+ const relyWindow = window.open(
50
+ url,
51
+ name,
52
+ `dialog=yes,top=${top}px,left=${left},width=${width !== undefined ? width : 400}px,height=${height !== undefined ? height : 600}px`,
53
+ );
54
+
55
+ // Fallback to iframe modal if:
56
+ // 1. window.open is blocked by browser
57
+ // 2. iOS Safari requires user interaction for window.open
58
+ if (!relyWindow) {
59
+ return null;
60
+ }
61
+
62
+ return relyWindow;
63
+ } catch (error) {
64
+ console.error("Failed to open window:", error);
65
+ return null;
66
+ }
67
+ };
68
+
69
+ export const closeWindow = () => {
70
+ try {
71
+ if (passkeyRelayWindow && !passkeyRelayWindow.closed) {
72
+ passkeyRelayWindow.close();
73
+ }
74
+ } catch (error) {
75
+ console.error("Failed to close window:", error);
76
+ } finally {
77
+ passkeyRelayWindow = null;
78
+ if (windowCloseInterval) {
79
+ clearInterval(windowCloseInterval);
80
+ windowCloseInterval = null;
81
+ }
82
+ }
83
+ };
84
+
85
+ // const addWindowCloseListener = (onClose: () => void) => {
86
+ // if (windowCloseInterval) {
87
+ // clearInterval(windowCloseInterval);
88
+ // }
89
+
90
+ // windowCloseInterval = setInterval(() => {
91
+ // if (passkeyRelayWindow && passkeyRelayWindow.closed) {
92
+ // console.log("Passkey window was closed by user");
93
+ // onClose();
94
+ // if (windowCloseInterval) {
95
+ // clearInterval(windowCloseInterval);
96
+ // windowCloseInterval = null;
97
+ // }
98
+ // passkeyRelayWindow = null;
99
+ // }
100
+ // }, 1000);
101
+ // };
102
+
103
+ // const removeWindowCloseListener = () => {
104
+ // if (windowCloseInterval) {
105
+ // clearInterval(windowCloseInterval);
106
+ // windowCloseInterval = null;
107
+ // }
108
+ // };
109
+
110
+ export const sendPasskeyMessage = async (message: any, tomoStage: TomoStage) => {
111
+ if (passkeyRelayWindow === null) {
112
+ throw new Error("Failed to open relay window");
113
+ }
114
+ const origin = RelayOrigins[tomoStage] || RelayOrigins["dev"];
115
+ passkeyRelayWindow.postMessage({ ...message, from: SENDER_NAME }, origin);
116
+ };
117
+
118
+ export const initPasskeyPage = async (config: CubeRelayConfig): Promise<UserToken> => {
119
+ return new Promise((resolve, reject) => {
120
+ config = (config || cache.get("cubeConfig")) as CubeRelayConfig;
121
+ if (!config.oidcToken || !config.tomoClientId) {
122
+ reject({ message: "oidcToken/tomoClientId is required" });
123
+ return;
124
+ }
125
+
126
+ const PASSKEY_ORIGIN = RelayOrigins[config.tomoStage];
127
+ if (!PASSKEY_ORIGIN) {
128
+ reject({ message: "tomoStage must be dev/pre/prod." });
129
+ return;
130
+ }
131
+
132
+ passkeyRelayWindow = openWindow({
133
+ url: `${PASSKEY_ORIGIN}/passkey?origin=${window.location.origin}`,
134
+ name: "relay-passkey",
135
+ width: 400,
136
+ height: 600,
137
+ });
138
+
139
+ const handleMessage = (event: MessageEvent) => {
140
+ const { type, data, from, error } = event.data;
141
+
142
+ if (type === "passkey-relay-loaded" && from === RELAY_NAME) {
143
+ sendPasskeyMessage({ type: "init", params: config }, config.tomoStage);
144
+ }
145
+
146
+ if (type === "passkey-relay-init" && from === RELAY_NAME) {
147
+ window.removeEventListener("message", handleMessage);
148
+ resolve(data);
149
+ }
150
+ // else if (type === "error" || type === "cancel") {
151
+ // window.removeEventListener("message", handleMessage);
152
+ // reject(new Error(error || "Passkey initialization cancelled"));
153
+ // }
154
+ };
155
+
156
+ // addWindowCloseListener(() => {
157
+ // window.removeEventListener("message", handleMessage);
158
+ // reject(new Error("Passkey window was closed by user"));
159
+ // });
160
+
161
+ window.addEventListener("message", handleMessage);
162
+ });
163
+ };
164
+
165
+ export const addPasskey = async (
166
+ params: { fidoName: string; receipt?: any },
167
+ config: CubeRelayConfig,
168
+ ): Promise<any> => {
169
+ if (!passkeyRelayWindow) {
170
+ await initPasskeyPage(config);
171
+ }
172
+
173
+ return new Promise((resolve, reject) => {
174
+ const handleMessage = (event: MessageEvent) => {
175
+ const { type, data, from, error } = event.data;
176
+
177
+ if (type === "passkey-relay-addFido" && from === RELAY_NAME) {
178
+ window.removeEventListener("message", handleMessage);
179
+ // removeWindowCloseListener();
180
+ resolve(data);
181
+ }
182
+ };
183
+
184
+ // addWindowCloseListener(() => {
185
+ // window.removeEventListener("message", handleMessage);
186
+ // reject(new Error("Add passkey window was closed by user"));
187
+ // });
188
+
189
+ window.addEventListener("message", handleMessage);
190
+ sendPasskeyMessage({ type: "addFido", params }, config.tomoStage);
191
+ });
192
+ };
193
+
194
+ export const verifyPasskey = async (params: { mfaId: string }, config: CubeRelayConfig): Promise<any> => {
195
+ if (!passkeyRelayWindow) {
196
+ await initPasskeyPage(config);
197
+ }
198
+ return new Promise((resolve, reject) => {
199
+ const handleMessage = (event: MessageEvent) => {
200
+ const { type, data: receipt, from, error } = event.data;
201
+
202
+ if (type === "passkey-relay-verify" && from === RELAY_NAME) {
203
+ window.removeEventListener("message", handleMessage);
204
+ // removeWindowCloseListener();
205
+ resolve(receipt);
206
+ }
207
+ // else if (type === "error" || type === "cancel") {
208
+ // window.removeEventListener("message", handleMessage);
209
+ // reject(new Error(error || "Passkey verification cancelled"));
210
+ // }
211
+ };
212
+
213
+ // addWindowCloseListener(() => {
214
+ // window.removeEventListener("message", handleMessage);
215
+ // reject(new Error("Verify passkey window was closed by user"));
216
+ // });
217
+
218
+ window.addEventListener("message", handleMessage);
219
+ sendPasskeyMessage({ type: "verify", params }, config.tomoStage);
220
+ });
221
+ };
@@ -0,0 +1,23 @@
1
+ import CryptoJS from "crypto-js";
2
+
3
+ /**
4
+ * Cube signature generation function
5
+ * @param cubeToken - Cube token
6
+ * @param salt - salt, default is "dev@tomo"
7
+ * @returns signed request object
8
+ */
9
+ export const generateCubeSignature = async (data: any, salt: string) => {
10
+ if (!salt) {
11
+ throw new Error("salt is required");
12
+ }
13
+
14
+ const timestamp = Date.now().toString();
15
+ const req = {
16
+ ...data,
17
+ timestamp,
18
+ };
19
+ const reqString = JSON.stringify(req);
20
+ const signature = CryptoJS.SHA256(reqString + salt).toString();
21
+
22
+ return { timestamp, signature };
23
+ };
package/src/types.ts ADDED
@@ -0,0 +1,269 @@
1
+ import { CubeStage, TomoStage } from "@tomo-inc/wallet-utils";
2
+
3
+ export type MfaType = "fido" | "emailOtp" | "totp";
4
+
5
+ export type SeedPhrase = any;
6
+
7
+ export interface MfaInfo {
8
+ success?: boolean;
9
+ message?: string;
10
+ data?: any;
11
+ error?: string;
12
+ id: string;
13
+ name: string;
14
+ type: MfaType;
15
+ status: {
16
+ allowed_approvers: string[];
17
+ allowed_mfa_types: string[];
18
+ approved_by: any;
19
+ count: number;
20
+ num_auth_factors: 1;
21
+ };
22
+ created_at: number;
23
+ created_by: string;
24
+ expires_at: number;
25
+ provenance: string;
26
+ receipt: any;
27
+ related_ids: string[];
28
+ request: {
29
+ body: any;
30
+ method: "DELETE" | "POST" | "PUT" | "DELETE" | "GET";
31
+ path: string;
32
+ };
33
+ verifyStatus?: "pending" | "approved" | "rejected";
34
+ mfaRequired?: {
35
+ fido: boolean;
36
+ totp: boolean;
37
+ emailOtp: boolean | number; //seconds
38
+ emailOtpRemainTime: number;
39
+ } | null;
40
+ }
41
+
42
+ export type BizType =
43
+ | "createSession"
44
+ | "addFido"
45
+ | "deleteFido"
46
+ | "registerTotp"
47
+ | "registerEmailOtp"
48
+ | "deleteTotp"
49
+ | "initExport"
50
+ | "completeExport";
51
+
52
+ export interface MfaReceipt {
53
+ mfaId: string;
54
+ mfaConf: string;
55
+ mfaOrgId: string;
56
+ }
57
+ export type MfaRequest = any;
58
+
59
+ export interface MfaAnswer {
60
+ totpCode?: string;
61
+ email?: string;
62
+ emailCode?: string;
63
+ }
64
+
65
+ export interface MfaVerifyResult {
66
+ success: boolean;
67
+ receipt: any;
68
+ error?: any;
69
+ }
70
+
71
+ export interface MfaFinishResult {
72
+ success: boolean;
73
+ error?: any;
74
+ }
75
+
76
+ export type AccountType = "google" | "x" | "email";
77
+
78
+ export interface CubeAccount {
79
+ userId: string;
80
+ name: string;
81
+ email: string;
82
+ otpEmail: string;
83
+ accountType: AccountType;
84
+ }
85
+
86
+ export interface MfaConfig {
87
+ noMfa: boolean;
88
+ hasMfa: boolean;
89
+ account: CubeAccount;
90
+ emailOtp: {
91
+ data: string;
92
+ enabled: boolean;
93
+ };
94
+ fido: {
95
+ data: FidoKey[];
96
+ enabled: boolean;
97
+ };
98
+ totp: {
99
+ data: any;
100
+ enabled: boolean;
101
+ };
102
+ }
103
+
104
+ export type OidcSessionResp = any;
105
+ export type CubeSignerClient = any;
106
+ export type ApiClient = any;
107
+
108
+ export interface CubeUserInfo {
109
+ user_id: string;
110
+ mfa: FidoKey[];
111
+ name?: string;
112
+ email?: string;
113
+ org_ids?: string[];
114
+ orgs?: any[];
115
+ verified_email?: {
116
+ email?: string;
117
+ updated_at?: number;
118
+ };
119
+ }
120
+
121
+ export interface TotpInfo {
122
+ id: string;
123
+ url: string;
124
+ secret: string;
125
+ issuer: string;
126
+ }
127
+
128
+ export interface FidoKey {
129
+ type: string;
130
+ id: string;
131
+ name: string;
132
+ updated_at: string;
133
+ status: string;
134
+ user_id: string;
135
+ discoverable: boolean;
136
+ created_at: number;
137
+ last_used_at: number;
138
+ aaguid: string;
139
+ }
140
+
141
+ export interface CubeIdentity {
142
+ id: string;
143
+ preferred_username?: string;
144
+ aud?: any;
145
+ email?: string;
146
+ identity?: {
147
+ iss: string;
148
+ sub: string;
149
+ };
150
+ user_info?: {
151
+ user_id?: string;
152
+ initialized?: boolean;
153
+ configured_mfa?: any[];
154
+ };
155
+ exp_epoch?: number;
156
+ }
157
+
158
+ export interface SeedPhraseExportInfo {
159
+ ready: boolean;
160
+ keyId?: string;
161
+ lastTime?: number;
162
+ duration?: number;
163
+ valid_epoch?: number;
164
+ exp_epoch?: number;
165
+ }
166
+
167
+ export interface CubeRes {
168
+ success: boolean;
169
+ error?: any;
170
+ data?: any;
171
+ message?: string;
172
+ }
173
+
174
+ export interface Mfa {
175
+ created_at?: number;
176
+ created_by: string;
177
+ expires_at: number;
178
+ id: string;
179
+ not_valid_until?: number;
180
+ provenance: "EditPolicy" | "Key" | "Role" | "KeyInRole" | "User";
181
+ receipt?: { confirmation: string; final_approver: string; timestamp: number };
182
+ related_ids?: string[];
183
+ request: { body?: Record<string, unknown>; method: string; path: string };
184
+ status: {
185
+ allowed_approvers: string[];
186
+ allowed_mfa_types?: string[];
187
+ approved_by: { [key: string]: object };
188
+ count: number;
189
+ num_auth_factors: number;
190
+ request_comparer?:
191
+ | "Eq"
192
+ | { EvmTx: { grace?: number; ignore_gas?: boolean; ignore_nonce?: boolean } }
193
+ | { SolanaTx: { ignore_blockhash?: boolean } };
194
+ };
195
+ }
196
+
197
+ export interface UserToken {
198
+ accessToken: string;
199
+ refreshToken: string;
200
+ expireTime: number;
201
+ }
202
+
203
+ export interface User {
204
+ userId: string;
205
+ userID?: string;
206
+ accountID?: string;
207
+ tenantID?: string;
208
+ username?: string;
209
+ nickname: string;
210
+ avatar: string;
211
+ freePasswordAuth?: string;
212
+ forbidden?: boolean;
213
+ deleted?: boolean;
214
+ }
215
+
216
+ export interface UserAddressType {
217
+ bitcoinP2pkhAddress: string;
218
+ bitcoinP2trAddress: string;
219
+ bitcoinP2shAddress: string;
220
+ bitcoinP2wpkhAddress: string;
221
+ ethereumAddress: string;
222
+ solanaAddress: string;
223
+ tronAddress: string;
224
+ tonAddressTest: string;
225
+ tonAddress: string;
226
+ tonPublicKey: string;
227
+ suiAddress: string;
228
+ }
229
+
230
+ export interface LoginRes {
231
+ userToken?: UserToken;
232
+ user: User;
233
+ accountBaseInfo: any;
234
+ accountWallet: any;
235
+ walletId?: string;
236
+ }
237
+
238
+ export enum LoginError {
239
+ FIDO_NOT_ALLOWED = "NotAllowedError",
240
+ MFA_NOT_APPROVED = "MFA not approved yet",
241
+ MFA_REQUIRED = "MFA should not be required after approval",
242
+ OIDC_TOKEN_NOT_FOUND = "Oidc token not found",
243
+ CUBE_IDENTITY_NOT_FOUND = "Cube identity not found",
244
+ CUBE_USER_INFO_NOT_FOUND = "Cube user info not found",
245
+ OIDC_TOKEN_EXPIRED = "ExpiredSignature",
246
+ FAILED_OPEN_POPUP = "Failed to open popup window",
247
+ OAUTH_LOGIN_TIMEOUT = "OAuth login timeout",
248
+ USER_CANCELLED_LOGIN = "User cancelled login",
249
+ }
250
+
251
+ export interface CubeConfig {
252
+ rpId?: string;
253
+ tomoStage: TomoStage;
254
+ cubeSalt?: string;
255
+ cubeStage?: CubeStage;
256
+ tomoClientId: string;
257
+ jwtToken?: string;
258
+ oidcToken?: string;
259
+ name?: string;
260
+ logo?: string;
261
+ }
262
+
263
+ export interface CubeConnectResult {
264
+ walletId: string;
265
+ session: object;
266
+ accountWallet: Record<string, any>;
267
+ token: object;
268
+ user: Record<string, any>;
269
+ }