@thru/wallet-store 0.2.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 +112 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/package.json +23 -0
- package/src/account-store.ts +91 -0
- package/src/connected-apps-store.ts +83 -0
- package/src/db.ts +18 -0
- package/src/index.ts +20 -0
- package/src/passkey-profiles.ts +191 -0
- package/src/schema.ts +34 -0
- package/src/types.ts +47 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# @thru/wallet-store
|
|
2
|
+
|
|
3
|
+
Browser-side IndexedDB storage layer for the Thru wallet. Provides typed helpers for persisting accounts, connected dApps, and passkey profiles in a single unified database. Replaces the legacy `@thru/indexed-db-stamper` package.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @thru/wallet-store
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
AccountStorage,
|
|
16
|
+
ConnectedAppsStorage,
|
|
17
|
+
loadPasskeyProfiles,
|
|
18
|
+
savePasskeyProfiles,
|
|
19
|
+
} from '@thru/wallet-store';
|
|
20
|
+
|
|
21
|
+
// Save and retrieve accounts
|
|
22
|
+
await AccountStorage.saveAccount({
|
|
23
|
+
index: 0,
|
|
24
|
+
label: 'Main',
|
|
25
|
+
publicKey: '...',
|
|
26
|
+
path: "m/44'/501'/0'/0'",
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
});
|
|
29
|
+
const accounts = await AccountStorage.getAccounts();
|
|
30
|
+
|
|
31
|
+
// Track connected dApps per account
|
|
32
|
+
await ConnectedAppsStorage.upsert({
|
|
33
|
+
accountId: 0,
|
|
34
|
+
appId: 'my-dapp',
|
|
35
|
+
origin: 'https://my-dapp.example',
|
|
36
|
+
metadata: { name: 'My dApp', icon: '...' },
|
|
37
|
+
});
|
|
38
|
+
const apps = await ConnectedAppsStorage.listByAccount(0);
|
|
39
|
+
|
|
40
|
+
// Manage passkey profiles
|
|
41
|
+
const store = await loadPasskeyProfiles();
|
|
42
|
+
if (store) {
|
|
43
|
+
await savePasskeyProfiles(store);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Key Capabilities
|
|
48
|
+
|
|
49
|
+
- Single shared IndexedDB database (`thru-wallet` v1) with lazy singleton connection
|
|
50
|
+
- **AccountStorage** -- CRUD for HD-derived account metadata (does not store private keys)
|
|
51
|
+
- **ConnectedAppsStorage** -- track which dApps are authorized per account, with upsert and indexed lookups
|
|
52
|
+
- **Passkey profiles** -- load, save, create, and update WebAuthn passkey profiles with built-in schema migration support
|
|
53
|
+
- Pure in-memory transform helpers (`updateProfilePasskey`, `updatePasskeyLastUsed`) that separate mutation from persistence
|
|
54
|
+
- SSR-safe: passkey helpers return `null` / `false` when `window` is undefined
|
|
55
|
+
|
|
56
|
+
## Database Schema
|
|
57
|
+
|
|
58
|
+
The package opens a single IndexedDB database named `thru-wallet` at version 1 with three object stores:
|
|
59
|
+
|
|
60
|
+
| Store | Key | Indexes | Purpose |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| `accounts` | `index` | `by-created` | HD wallet account metadata |
|
|
63
|
+
| `connectedApps` | `accountId:appId` | `by-account`, `by-updated` | Authorized dApp connections |
|
|
64
|
+
| `passkeyProfiles` | `id` | -- | WebAuthn passkey profiles and settings |
|
|
65
|
+
|
|
66
|
+
Access the raw database connection when needed:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { getUnifiedDB } from '@thru/wallet-store';
|
|
70
|
+
|
|
71
|
+
const db = await getUnifiedDB();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API Reference
|
|
75
|
+
|
|
76
|
+
### AccountStorage
|
|
77
|
+
|
|
78
|
+
| Method | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `saveAccount(account)` | Insert or update an account record |
|
|
81
|
+
| `getAccounts()` | List all accounts sorted by index |
|
|
82
|
+
| `getAccount(index)` | Fetch a single account by its BIP-44 index |
|
|
83
|
+
| `updateAccountLabel(index, label)` | Rename an account |
|
|
84
|
+
| `getNextAccountIndex()` | Return the next unused account index |
|
|
85
|
+
| `hasAccounts()` | Check whether any accounts exist |
|
|
86
|
+
| `getAccountCount()` | Return total account count |
|
|
87
|
+
| `clearAccounts()` | Delete all account records |
|
|
88
|
+
|
|
89
|
+
### ConnectedAppsStorage
|
|
90
|
+
|
|
91
|
+
| Method | Description |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `upsert(app)` | Insert or update a connected app record |
|
|
94
|
+
| `listByAccount(accountId)` | List apps for an account, most recently updated first |
|
|
95
|
+
| `get(accountId, appId)` | Fetch a single connection record |
|
|
96
|
+
| `remove(accountId, appId)` | Delete a connection |
|
|
97
|
+
| `clear()` | Delete all connected app records |
|
|
98
|
+
|
|
99
|
+
### Passkey Profiles
|
|
100
|
+
|
|
101
|
+
| Function | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `loadPasskeyProfiles()` | Load all profiles and settings from IndexedDB |
|
|
104
|
+
| `savePasskeyProfiles(store)` | Persist the full profile store (replaces all records) |
|
|
105
|
+
| `createDefaultProfileStore()` | Create an in-memory store with one empty default profile |
|
|
106
|
+
| `updateProfilePasskey(store, index, passkey)` | Pure transform: attach passkey metadata to a profile |
|
|
107
|
+
| `updatePasskeyLastUsed(store, index)` | Pure transform: bump `lastUsedAt` timestamp |
|
|
108
|
+
|
|
109
|
+
## Dependencies
|
|
110
|
+
|
|
111
|
+
- [`idb`](https://github.com/jakearchibald/idb) -- Promise-based IndexedDB wrapper
|
|
112
|
+
- `@thru/chain-interfaces` -- shared type definitions
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { AddressType, AppMetadata, ConnectedApp } from '@thru/chain-interfaces';
|
|
2
|
+
import { IDBPDatabase } from 'idb';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stored account representation in IndexedDB
|
|
6
|
+
*/
|
|
7
|
+
interface StoredAccount {
|
|
8
|
+
index: number;
|
|
9
|
+
label: string;
|
|
10
|
+
publicKey: string;
|
|
11
|
+
path: string;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
addressType?: AddressType;
|
|
14
|
+
publicKeyRawBase64?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Account storage management using the unified IndexedDB.
|
|
19
|
+
* Stores metadata for each derived account (but NOT private keys).
|
|
20
|
+
*/
|
|
21
|
+
declare class AccountStorage {
|
|
22
|
+
/**
|
|
23
|
+
* Save a new account to storage
|
|
24
|
+
*/
|
|
25
|
+
static saveAccount(account: StoredAccount): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Get all accounts, sorted by index (ascending)
|
|
28
|
+
*/
|
|
29
|
+
static getAccounts(): Promise<StoredAccount[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Get a specific account by index
|
|
32
|
+
*/
|
|
33
|
+
static getAccount(index: number): Promise<StoredAccount | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Update an account's label
|
|
36
|
+
*/
|
|
37
|
+
static updateAccountLabel(index: number, label: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get the next available account index
|
|
40
|
+
*/
|
|
41
|
+
static getNextAccountIndex(): Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* Check if any accounts exist
|
|
44
|
+
*/
|
|
45
|
+
static hasAccounts(): Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Get total number of accounts
|
|
48
|
+
*/
|
|
49
|
+
static getAccountCount(): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* Clear all accounts (use with caution!)
|
|
52
|
+
*/
|
|
53
|
+
static clearAccounts(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ConnectedAppUpsert {
|
|
57
|
+
accountId: number;
|
|
58
|
+
appId: string;
|
|
59
|
+
origin: string;
|
|
60
|
+
metadata: AppMetadata;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Storage helper for connected dApps per account
|
|
64
|
+
*/
|
|
65
|
+
declare class ConnectedAppsStorage {
|
|
66
|
+
private static getDB;
|
|
67
|
+
static upsert(app: ConnectedAppUpsert): Promise<ConnectedApp>;
|
|
68
|
+
static listByAccount(accountId: number): Promise<ConnectedApp[]>;
|
|
69
|
+
static remove(accountId: number, appId: string): Promise<void>;
|
|
70
|
+
static clear(): Promise<void>;
|
|
71
|
+
static get(accountId: number, appId: string): Promise<ConnectedApp | null>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the shared unified database connection.
|
|
76
|
+
*/
|
|
77
|
+
declare function getUnifiedDB(): Promise<IDBPDatabase>;
|
|
78
|
+
|
|
79
|
+
declare const DB_NAME = "thru-wallet";
|
|
80
|
+
declare const DB_VERSION = 1;
|
|
81
|
+
declare enum StoreName {
|
|
82
|
+
CONNECTED_APPS = "connectedApps",
|
|
83
|
+
ACCOUNTS = "accounts",
|
|
84
|
+
PASSKEY_PROFILES = "passkeyProfiles"
|
|
85
|
+
}
|
|
86
|
+
interface ConnectedAppData {
|
|
87
|
+
key: string;
|
|
88
|
+
accountId: number;
|
|
89
|
+
appId: string;
|
|
90
|
+
origin: string;
|
|
91
|
+
metadata: AppMetadata;
|
|
92
|
+
connectedAt: number;
|
|
93
|
+
updatedAt: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface PasskeyMetadata {
|
|
97
|
+
credentialId: string;
|
|
98
|
+
publicKeyX: string;
|
|
99
|
+
publicKeyY: string;
|
|
100
|
+
rpId: string;
|
|
101
|
+
label?: string;
|
|
102
|
+
createdAt: string;
|
|
103
|
+
lastUsedAt: string;
|
|
104
|
+
}
|
|
105
|
+
interface PasskeyProfile {
|
|
106
|
+
id: string;
|
|
107
|
+
label: string;
|
|
108
|
+
passkey: PasskeyMetadata | null;
|
|
109
|
+
createdAt: string;
|
|
110
|
+
lastUsedAt: string | null;
|
|
111
|
+
}
|
|
112
|
+
interface PasskeyProfileStore {
|
|
113
|
+
profiles: PasskeyProfile[];
|
|
114
|
+
selectedIndex: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Load all passkey profiles and settings from IndexedDB.
|
|
118
|
+
* Returns null if no profiles exist or if the DB is unavailable.
|
|
119
|
+
*/
|
|
120
|
+
declare function loadPasskeyProfiles(): Promise<PasskeyProfileStore | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Save all passkey profiles and settings to IndexedDB.
|
|
123
|
+
* Returns true on success, false on failure.
|
|
124
|
+
*/
|
|
125
|
+
declare function savePasskeyProfiles(store: PasskeyProfileStore): Promise<boolean>;
|
|
126
|
+
/**
|
|
127
|
+
* Create a default profile store with one empty profile.
|
|
128
|
+
*/
|
|
129
|
+
declare function createDefaultProfileStore(): PasskeyProfileStore;
|
|
130
|
+
/**
|
|
131
|
+
* Update a profile's passkey metadata (pure in-memory transform).
|
|
132
|
+
*/
|
|
133
|
+
declare function updateProfilePasskey(store: PasskeyProfileStore, profileIndex: number, passkey: PasskeyMetadata): PasskeyProfileStore;
|
|
134
|
+
/**
|
|
135
|
+
* Update lastUsedAt timestamp for a profile's passkey (pure in-memory transform).
|
|
136
|
+
*/
|
|
137
|
+
declare function updatePasskeyLastUsed(store: PasskeyProfileStore, profileIndex: number): PasskeyProfileStore;
|
|
138
|
+
|
|
139
|
+
export { AccountStorage, type ConnectedAppData, ConnectedAppsStorage, DB_NAME, DB_VERSION, type PasskeyMetadata, type PasskeyProfile, type PasskeyProfileStore, StoreName, type StoredAccount, createDefaultProfileStore, getUnifiedDB, loadPasskeyProfiles, savePasskeyProfiles, updatePasskeyLastUsed, updateProfilePasskey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { openDB } from 'idb';
|
|
2
|
+
|
|
3
|
+
// src/db.ts
|
|
4
|
+
|
|
5
|
+
// src/schema.ts
|
|
6
|
+
var DB_NAME = "thru-wallet";
|
|
7
|
+
var DB_VERSION = 1;
|
|
8
|
+
var StoreName = /* @__PURE__ */ ((StoreName2) => {
|
|
9
|
+
StoreName2["CONNECTED_APPS"] = "connectedApps";
|
|
10
|
+
StoreName2["ACCOUNTS"] = "accounts";
|
|
11
|
+
StoreName2["PASSKEY_PROFILES"] = "passkeyProfiles";
|
|
12
|
+
return StoreName2;
|
|
13
|
+
})(StoreName || {});
|
|
14
|
+
function initializeSchema(db) {
|
|
15
|
+
const connectedApps = db.createObjectStore("connectedApps" /* CONNECTED_APPS */, { keyPath: "key" });
|
|
16
|
+
connectedApps.createIndex("by-account", "accountId", { unique: false });
|
|
17
|
+
connectedApps.createIndex("by-updated", "updatedAt", { unique: false });
|
|
18
|
+
const accounts = db.createObjectStore("accounts" /* ACCOUNTS */, { keyPath: "index" });
|
|
19
|
+
accounts.createIndex("by-created", "createdAt", { unique: false });
|
|
20
|
+
db.createObjectStore("passkeyProfiles" /* PASSKEY_PROFILES */, { keyPath: "id" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/db.ts
|
|
24
|
+
var dbPromise = null;
|
|
25
|
+
function getUnifiedDB() {
|
|
26
|
+
if (!dbPromise) {
|
|
27
|
+
dbPromise = openDB(DB_NAME, DB_VERSION, {
|
|
28
|
+
upgrade(db) {
|
|
29
|
+
initializeSchema(db);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return dbPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/account-store.ts
|
|
37
|
+
var STORE_NAME = "accounts" /* ACCOUNTS */;
|
|
38
|
+
var AccountStorage = class {
|
|
39
|
+
/**
|
|
40
|
+
* Save a new account to storage
|
|
41
|
+
*/
|
|
42
|
+
static async saveAccount(account) {
|
|
43
|
+
const db = await getUnifiedDB();
|
|
44
|
+
await db.put(STORE_NAME, account);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get all accounts, sorted by index (ascending)
|
|
48
|
+
*/
|
|
49
|
+
static async getAccounts() {
|
|
50
|
+
const db = await getUnifiedDB();
|
|
51
|
+
const accounts = await db.getAll(STORE_NAME);
|
|
52
|
+
return accounts.sort((a, b) => a.index - b.index);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get a specific account by index
|
|
56
|
+
*/
|
|
57
|
+
static async getAccount(index) {
|
|
58
|
+
const db = await getUnifiedDB();
|
|
59
|
+
const account = await db.get(STORE_NAME, index);
|
|
60
|
+
return account || null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Update an account's label
|
|
64
|
+
*/
|
|
65
|
+
static async updateAccountLabel(index, label) {
|
|
66
|
+
const db = await getUnifiedDB();
|
|
67
|
+
const account = await db.get(STORE_NAME, index);
|
|
68
|
+
if (!account) {
|
|
69
|
+
throw new Error(`Account ${index} not found`);
|
|
70
|
+
}
|
|
71
|
+
account.label = label;
|
|
72
|
+
await db.put(STORE_NAME, account);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the next available account index
|
|
76
|
+
*/
|
|
77
|
+
static async getNextAccountIndex() {
|
|
78
|
+
const accounts = await this.getAccounts();
|
|
79
|
+
if (accounts.length === 0) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const maxIndex = Math.max(...accounts.map((a) => a.index));
|
|
83
|
+
return maxIndex + 1;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check if any accounts exist
|
|
87
|
+
*/
|
|
88
|
+
static async hasAccounts() {
|
|
89
|
+
const db = await getUnifiedDB();
|
|
90
|
+
const count = await db.count(STORE_NAME);
|
|
91
|
+
return count > 0;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get total number of accounts
|
|
95
|
+
*/
|
|
96
|
+
static async getAccountCount() {
|
|
97
|
+
const db = await getUnifiedDB();
|
|
98
|
+
return await db.count(STORE_NAME);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Clear all accounts (use with caution!)
|
|
102
|
+
*/
|
|
103
|
+
static async clearAccounts() {
|
|
104
|
+
const db = await getUnifiedDB();
|
|
105
|
+
await db.clear(STORE_NAME);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// src/connected-apps-store.ts
|
|
110
|
+
var STORE_NAME2 = "connectedApps" /* CONNECTED_APPS */;
|
|
111
|
+
function createKey(accountId, appId) {
|
|
112
|
+
return `${accountId}:${appId}`;
|
|
113
|
+
}
|
|
114
|
+
function toDomain(record) {
|
|
115
|
+
return {
|
|
116
|
+
accountId: record.accountId,
|
|
117
|
+
appId: record.appId,
|
|
118
|
+
origin: record.origin,
|
|
119
|
+
metadata: record.metadata,
|
|
120
|
+
connectedAt: record.connectedAt,
|
|
121
|
+
updatedAt: record.updatedAt
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
var ConnectedAppsStorage = class {
|
|
125
|
+
static async getDB() {
|
|
126
|
+
return getUnifiedDB();
|
|
127
|
+
}
|
|
128
|
+
static async upsert(app) {
|
|
129
|
+
const db = await this.getDB();
|
|
130
|
+
const key = createKey(app.accountId, app.appId);
|
|
131
|
+
const existing = await db.get(STORE_NAME2, key);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const record = {
|
|
134
|
+
key,
|
|
135
|
+
accountId: app.accountId,
|
|
136
|
+
appId: app.appId,
|
|
137
|
+
origin: app.origin,
|
|
138
|
+
metadata: app.metadata,
|
|
139
|
+
connectedAt: existing?.connectedAt ?? now,
|
|
140
|
+
updatedAt: now
|
|
141
|
+
};
|
|
142
|
+
await db.put(STORE_NAME2, record);
|
|
143
|
+
return toDomain(record);
|
|
144
|
+
}
|
|
145
|
+
static async listByAccount(accountId) {
|
|
146
|
+
const db = await this.getDB();
|
|
147
|
+
const records = await db.getAllFromIndex(STORE_NAME2, "by-account", IDBKeyRange.only(accountId));
|
|
148
|
+
return records.sort((a, b) => b.updatedAt - a.updatedAt).map(toDomain);
|
|
149
|
+
}
|
|
150
|
+
static async remove(accountId, appId) {
|
|
151
|
+
const db = await this.getDB();
|
|
152
|
+
await db.delete(STORE_NAME2, createKey(accountId, appId));
|
|
153
|
+
}
|
|
154
|
+
static async clear() {
|
|
155
|
+
const db = await this.getDB();
|
|
156
|
+
await db.clear(STORE_NAME2);
|
|
157
|
+
}
|
|
158
|
+
static async get(accountId, appId) {
|
|
159
|
+
const db = await this.getDB();
|
|
160
|
+
const record = await db.get(STORE_NAME2, createKey(accountId, appId));
|
|
161
|
+
return record ? toDomain(record) : null;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/passkey-profiles.ts
|
|
166
|
+
var CURRENT_PROFILE_VERSION = 1;
|
|
167
|
+
var SETTINGS_KEY = "settings";
|
|
168
|
+
async function loadPasskeyProfiles() {
|
|
169
|
+
if (typeof window === "undefined") return null;
|
|
170
|
+
try {
|
|
171
|
+
const db = await getUnifiedDB();
|
|
172
|
+
const allRecords = await db.getAll("passkeyProfiles" /* PASSKEY_PROFILES */);
|
|
173
|
+
let settings = null;
|
|
174
|
+
const profiles = [];
|
|
175
|
+
for (const record of allRecords) {
|
|
176
|
+
if (record.id === SETTINGS_KEY) {
|
|
177
|
+
settings = record;
|
|
178
|
+
} else if (record.id) {
|
|
179
|
+
profiles.push(record);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (profiles.length === 0) return null;
|
|
183
|
+
if (settings && settings.version < CURRENT_PROFILE_VERSION) {
|
|
184
|
+
migrateProfiles(profiles, settings.version);
|
|
185
|
+
settings = { ...settings, version: CURRENT_PROFILE_VERSION };
|
|
186
|
+
await db.put("passkeyProfiles" /* PASSKEY_PROFILES */, settings);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
profiles,
|
|
190
|
+
selectedIndex: settings?.selectedIndex ?? 0
|
|
191
|
+
};
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function savePasskeyProfiles(store) {
|
|
197
|
+
if (typeof window === "undefined") return false;
|
|
198
|
+
try {
|
|
199
|
+
const db = await getUnifiedDB();
|
|
200
|
+
const tx = db.transaction("passkeyProfiles" /* PASSKEY_PROFILES */, "readwrite");
|
|
201
|
+
await tx.store.clear();
|
|
202
|
+
for (const profile of store.profiles) {
|
|
203
|
+
await tx.store.put(profile);
|
|
204
|
+
}
|
|
205
|
+
const settings = {
|
|
206
|
+
id: SETTINGS_KEY,
|
|
207
|
+
selectedIndex: store.selectedIndex,
|
|
208
|
+
version: CURRENT_PROFILE_VERSION
|
|
209
|
+
};
|
|
210
|
+
await tx.store.put(settings);
|
|
211
|
+
await tx.done;
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function createDefaultProfileStore() {
|
|
218
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
219
|
+
const profileId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : String(Date.now());
|
|
220
|
+
return {
|
|
221
|
+
profiles: [
|
|
222
|
+
{
|
|
223
|
+
id: profileId,
|
|
224
|
+
label: "Default Profile",
|
|
225
|
+
passkey: null,
|
|
226
|
+
createdAt: now,
|
|
227
|
+
lastUsedAt: null
|
|
228
|
+
}
|
|
229
|
+
],
|
|
230
|
+
selectedIndex: 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function updateProfilePasskey(store, profileIndex, passkey) {
|
|
234
|
+
const updatedProfiles = [...store.profiles];
|
|
235
|
+
const current = updatedProfiles[profileIndex];
|
|
236
|
+
if (!current) {
|
|
237
|
+
return store;
|
|
238
|
+
}
|
|
239
|
+
updatedProfiles[profileIndex] = {
|
|
240
|
+
...current,
|
|
241
|
+
passkey,
|
|
242
|
+
lastUsedAt: passkey.lastUsedAt
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
...store,
|
|
246
|
+
profiles: updatedProfiles
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function updatePasskeyLastUsed(store, profileIndex) {
|
|
250
|
+
const updatedProfiles = [...store.profiles];
|
|
251
|
+
const current = updatedProfiles[profileIndex];
|
|
252
|
+
if (!current?.passkey) {
|
|
253
|
+
return store;
|
|
254
|
+
}
|
|
255
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
256
|
+
updatedProfiles[profileIndex] = {
|
|
257
|
+
...current,
|
|
258
|
+
passkey: {
|
|
259
|
+
...current.passkey,
|
|
260
|
+
lastUsedAt: now
|
|
261
|
+
},
|
|
262
|
+
lastUsedAt: now
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
...store,
|
|
266
|
+
profiles: updatedProfiles
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function migrateProfiles(_profiles, _fromVersion) {
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { AccountStorage, ConnectedAppsStorage, DB_NAME, DB_VERSION, StoreName, createDefaultProfileStore, getUnifiedDB, loadPasskeyProfiles, savePasskeyProfiles, updatePasskeyLastUsed, updateProfilePasskey };
|
|
273
|
+
//# sourceMappingURL=index.js.map
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts","../src/db.ts","../src/account-store.ts","../src/connected-apps-store.ts","../src/passkey-profiles.ts"],"names":["StoreName","STORE_NAME"],"mappings":"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","file":"index.js","sourcesContent":["import type { AppMetadata } from '@thru/chain-interfaces';\n\nexport const DB_NAME = 'thru-wallet';\nexport const DB_VERSION = 1;\n\nexport enum StoreName {\n CONNECTED_APPS = 'connectedApps',\n ACCOUNTS = 'accounts',\n PASSKEY_PROFILES = 'passkeyProfiles',\n}\n\nexport interface ConnectedAppData {\n key: string; // `${accountId}:${appId}`\n accountId: number;\n appId: string;\n origin: string;\n metadata: AppMetadata;\n connectedAt: number;\n updatedAt: number;\n}\n\n/**\n * Initialize database schema.\n */\nexport function initializeSchema(db: IDBDatabase): void {\n const connectedApps = db.createObjectStore(StoreName.CONNECTED_APPS, { keyPath: 'key' });\n connectedApps.createIndex('by-account', 'accountId', { unique: false });\n connectedApps.createIndex('by-updated', 'updatedAt', { unique: false });\n\n const accounts = db.createObjectStore(StoreName.ACCOUNTS, { keyPath: 'index' });\n accounts.createIndex('by-created', 'createdAt', { unique: false });\n\n db.createObjectStore(StoreName.PASSKEY_PROFILES, { keyPath: 'id' });\n}\n","import { openDB, type IDBPDatabase } from 'idb';\nimport { DB_NAME, DB_VERSION, initializeSchema } from './schema';\n\nlet dbPromise: Promise<IDBPDatabase> | null = null;\n\n/**\n * Returns the shared unified database connection.\n */\nexport function getUnifiedDB(): Promise<IDBPDatabase> {\n if (!dbPromise) {\n dbPromise = openDB(DB_NAME, DB_VERSION, {\n upgrade(db) {\n initializeSchema(db as unknown as IDBDatabase);\n },\n });\n }\n return dbPromise;\n}\n","import { getUnifiedDB } from './db';\nimport { StoreName } from './schema';\nimport { StoredAccount } from './types';\n\nconst STORE_NAME = StoreName.ACCOUNTS;\n\n/**\n * Account storage management using the unified IndexedDB.\n * Stores metadata for each derived account (but NOT private keys).\n */\nexport class AccountStorage {\n /**\n * Save a new account to storage\n */\n static async saveAccount(account: StoredAccount): Promise<void> {\n const db = await getUnifiedDB();\n await db.put(STORE_NAME, account);\n }\n\n /**\n * Get all accounts, sorted by index (ascending)\n */\n static async getAccounts(): Promise<StoredAccount[]> {\n const db = await getUnifiedDB();\n const accounts = await db.getAll(STORE_NAME) as StoredAccount[];\n return accounts.sort((a, b) => a.index - b.index);\n }\n\n /**\n * Get a specific account by index\n */\n static async getAccount(index: number): Promise<StoredAccount | null> {\n const db = await getUnifiedDB();\n const account = await db.get(STORE_NAME, index) as StoredAccount | undefined;\n return account || null;\n }\n\n /**\n * Update an account's label\n */\n static async updateAccountLabel(index: number, label: string): Promise<void> {\n const db = await getUnifiedDB();\n const account = await db.get(STORE_NAME, index) as StoredAccount | undefined;\n\n if (!account) {\n throw new Error(`Account ${index} not found`);\n }\n\n account.label = label;\n await db.put(STORE_NAME, account);\n }\n\n /**\n * Get the next available account index\n */\n static async getNextAccountIndex(): Promise<number> {\n const accounts = await this.getAccounts();\n\n if (accounts.length === 0) {\n return 0;\n }\n\n const maxIndex = Math.max(...accounts.map(a => a.index));\n return maxIndex + 1;\n }\n\n /**\n * Check if any accounts exist\n */\n static async hasAccounts(): Promise<boolean> {\n const db = await getUnifiedDB();\n const count = await db.count(STORE_NAME);\n return count > 0;\n }\n\n /**\n * Get total number of accounts\n */\n static async getAccountCount(): Promise<number> {\n const db = await getUnifiedDB();\n return await db.count(STORE_NAME);\n }\n\n /**\n * Clear all accounts (use with caution!)\n */\n static async clearAccounts(): Promise<void> {\n const db = await getUnifiedDB();\n await db.clear(STORE_NAME);\n }\n}\n","import { getUnifiedDB } from './db';\nimport {\n StoreName,\n type ConnectedAppData,\n} from './schema';\nimport type { ConnectedApp, AppMetadata } from '@thru/chain-interfaces';\n\nconst STORE_NAME = StoreName.CONNECTED_APPS;\n\nfunction createKey(accountId: number, appId: string): string {\n return `${accountId}:${appId}`;\n}\n\nfunction toDomain(record: ConnectedAppData): ConnectedApp {\n return {\n accountId: record.accountId,\n appId: record.appId,\n origin: record.origin,\n metadata: record.metadata,\n connectedAt: record.connectedAt,\n updatedAt: record.updatedAt,\n };\n}\n\nexport interface ConnectedAppUpsert {\n accountId: number;\n appId: string;\n origin: string;\n metadata: AppMetadata;\n}\n\n/**\n * Storage helper for connected dApps per account\n */\nexport class ConnectedAppsStorage {\n private static async getDB() {\n return getUnifiedDB();\n }\n\n static async upsert(app: ConnectedAppUpsert): Promise<ConnectedApp> {\n const db = await this.getDB();\n const key = createKey(app.accountId, app.appId);\n const existing = (await db.get(STORE_NAME, key)) as ConnectedAppData | undefined;\n const now = Date.now();\n\n const record: ConnectedAppData = {\n key,\n accountId: app.accountId,\n appId: app.appId,\n origin: app.origin,\n metadata: app.metadata,\n connectedAt: existing?.connectedAt ?? now,\n updatedAt: now,\n };\n\n await db.put(STORE_NAME, record);\n return toDomain(record);\n }\n\n static async listByAccount(accountId: number): Promise<ConnectedApp[]> {\n const db = await this.getDB();\n const records = (await db.getAllFromIndex(STORE_NAME, 'by-account', IDBKeyRange.only(accountId))) as ConnectedAppData[];\n return records\n .sort((a, b) => b.updatedAt - a.updatedAt)\n .map(toDomain);\n }\n\n static async remove(accountId: number, appId: string): Promise<void> {\n const db = await this.getDB();\n await db.delete(STORE_NAME, createKey(accountId, appId));\n }\n\n static async clear(): Promise<void> {\n const db = await this.getDB();\n await db.clear(STORE_NAME);\n }\n\n static async get(accountId: number, appId: string): Promise<ConnectedApp | null> {\n const db = await this.getDB();\n const record = (await db.get(STORE_NAME, createKey(accountId, appId))) as ConnectedAppData | undefined;\n return record ? toDomain(record) : null;\n }\n}\n","import { getUnifiedDB } from './db';\nimport { StoreName } from './schema';\nimport type { PasskeyProfileRecord, PasskeyStoreSettings } from './types';\n\nconst CURRENT_PROFILE_VERSION = 1;\nconst SETTINGS_KEY = 'settings';\n\nexport interface PasskeyMetadata {\n credentialId: string;\n publicKeyX: string;\n publicKeyY: string;\n rpId: string;\n label?: string;\n createdAt: string;\n lastUsedAt: string;\n}\n\nexport interface PasskeyProfile {\n id: string;\n label: string;\n passkey: PasskeyMetadata | null;\n createdAt: string;\n lastUsedAt: string | null;\n}\n\nexport interface PasskeyProfileStore {\n profiles: PasskeyProfile[];\n selectedIndex: number;\n}\n\n/**\n * Load all passkey profiles and settings from IndexedDB.\n * Returns null if no profiles exist or if the DB is unavailable.\n */\nexport async function loadPasskeyProfiles(): Promise<PasskeyProfileStore | null> {\n if (typeof window === 'undefined') return null;\n\n try {\n const db = await getUnifiedDB();\n const allRecords = await db.getAll(StoreName.PASSKEY_PROFILES);\n\n let settings: PasskeyStoreSettings | null = null;\n const profiles: PasskeyProfile[] = [];\n\n for (const record of allRecords) {\n if (record.id === SETTINGS_KEY) {\n settings = record as PasskeyStoreSettings;\n } else if (record.id) {\n profiles.push(record as PasskeyProfile);\n }\n }\n\n if (profiles.length === 0) return null;\n\n // Run schema migration if needed\n if (settings && settings.version < CURRENT_PROFILE_VERSION) {\n migrateProfiles(profiles, settings.version);\n settings = { ...settings, version: CURRENT_PROFILE_VERSION };\n await db.put(StoreName.PASSKEY_PROFILES, settings);\n }\n\n return {\n profiles,\n selectedIndex: settings?.selectedIndex ?? 0,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Save all passkey profiles and settings to IndexedDB.\n * Returns true on success, false on failure.\n */\nexport async function savePasskeyProfiles(store: PasskeyProfileStore): Promise<boolean> {\n if (typeof window === 'undefined') return false;\n\n try {\n const db = await getUnifiedDB();\n const tx = db.transaction(StoreName.PASSKEY_PROFILES, 'readwrite');\n\n // Clear all existing records\n await tx.store.clear();\n\n // Write profiles\n for (const profile of store.profiles) {\n await tx.store.put(profile as PasskeyProfileRecord);\n }\n\n // Write settings\n const settings: PasskeyStoreSettings = {\n id: SETTINGS_KEY,\n selectedIndex: store.selectedIndex,\n version: CURRENT_PROFILE_VERSION,\n };\n await tx.store.put(settings);\n\n await tx.done;\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create a default profile store with one empty profile.\n */\nexport function createDefaultProfileStore(): PasskeyProfileStore {\n const now = new Date().toISOString();\n const profileId = typeof crypto !== 'undefined' && 'randomUUID' in crypto\n ? crypto.randomUUID()\n : String(Date.now());\n\n return {\n profiles: [\n {\n id: profileId,\n label: 'Default Profile',\n passkey: null,\n createdAt: now,\n lastUsedAt: null,\n },\n ],\n selectedIndex: 0,\n };\n}\n\n/**\n * Update a profile's passkey metadata (pure in-memory transform).\n */\nexport function updateProfilePasskey(\n store: PasskeyProfileStore,\n profileIndex: number,\n passkey: PasskeyMetadata\n): PasskeyProfileStore {\n const updatedProfiles = [...store.profiles];\n const current = updatedProfiles[profileIndex];\n if (!current) {\n return store;\n }\n\n updatedProfiles[profileIndex] = {\n ...current,\n passkey,\n lastUsedAt: passkey.lastUsedAt,\n };\n\n return {\n ...store,\n profiles: updatedProfiles,\n };\n}\n\n/**\n * Update lastUsedAt timestamp for a profile's passkey (pure in-memory transform).\n */\nexport function updatePasskeyLastUsed(\n store: PasskeyProfileStore,\n profileIndex: number\n): PasskeyProfileStore {\n const updatedProfiles = [...store.profiles];\n const current = updatedProfiles[profileIndex];\n if (!current?.passkey) {\n return store;\n }\n\n const now = new Date().toISOString();\n updatedProfiles[profileIndex] = {\n ...current,\n passkey: {\n ...current.passkey,\n lastUsedAt: now,\n },\n lastUsedAt: now,\n };\n\n return {\n ...store,\n profiles: updatedProfiles,\n };\n}\n\n/**\n * Migrate profile data from an older schema version to current.\n * Mutates profiles in-place for efficiency.\n */\nfunction migrateProfiles(_profiles: PasskeyProfile[], _fromVersion: number): void {\n // Currently at version 1, no migrations needed yet.\n // Future migrations would go here:\n // if (fromVersion < 2) { ... }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thru/wallet-store",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"idb": "^8.0.3",
|
|
15
|
+
"@thru/chain-interfaces": "0.2.1"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"lint": "eslint src/",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { getUnifiedDB } from './db';
|
|
2
|
+
import { StoreName } from './schema';
|
|
3
|
+
import { StoredAccount } from './types';
|
|
4
|
+
|
|
5
|
+
const STORE_NAME = StoreName.ACCOUNTS;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Account storage management using the unified IndexedDB.
|
|
9
|
+
* Stores metadata for each derived account (but NOT private keys).
|
|
10
|
+
*/
|
|
11
|
+
export class AccountStorage {
|
|
12
|
+
/**
|
|
13
|
+
* Save a new account to storage
|
|
14
|
+
*/
|
|
15
|
+
static async saveAccount(account: StoredAccount): Promise<void> {
|
|
16
|
+
const db = await getUnifiedDB();
|
|
17
|
+
await db.put(STORE_NAME, account);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get all accounts, sorted by index (ascending)
|
|
22
|
+
*/
|
|
23
|
+
static async getAccounts(): Promise<StoredAccount[]> {
|
|
24
|
+
const db = await getUnifiedDB();
|
|
25
|
+
const accounts = await db.getAll(STORE_NAME) as StoredAccount[];
|
|
26
|
+
return accounts.sort((a, b) => a.index - b.index);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a specific account by index
|
|
31
|
+
*/
|
|
32
|
+
static async getAccount(index: number): Promise<StoredAccount | null> {
|
|
33
|
+
const db = await getUnifiedDB();
|
|
34
|
+
const account = await db.get(STORE_NAME, index) as StoredAccount | undefined;
|
|
35
|
+
return account || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Update an account's label
|
|
40
|
+
*/
|
|
41
|
+
static async updateAccountLabel(index: number, label: string): Promise<void> {
|
|
42
|
+
const db = await getUnifiedDB();
|
|
43
|
+
const account = await db.get(STORE_NAME, index) as StoredAccount | undefined;
|
|
44
|
+
|
|
45
|
+
if (!account) {
|
|
46
|
+
throw new Error(`Account ${index} not found`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
account.label = label;
|
|
50
|
+
await db.put(STORE_NAME, account);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the next available account index
|
|
55
|
+
*/
|
|
56
|
+
static async getNextAccountIndex(): Promise<number> {
|
|
57
|
+
const accounts = await this.getAccounts();
|
|
58
|
+
|
|
59
|
+
if (accounts.length === 0) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const maxIndex = Math.max(...accounts.map(a => a.index));
|
|
64
|
+
return maxIndex + 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if any accounts exist
|
|
69
|
+
*/
|
|
70
|
+
static async hasAccounts(): Promise<boolean> {
|
|
71
|
+
const db = await getUnifiedDB();
|
|
72
|
+
const count = await db.count(STORE_NAME);
|
|
73
|
+
return count > 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get total number of accounts
|
|
78
|
+
*/
|
|
79
|
+
static async getAccountCount(): Promise<number> {
|
|
80
|
+
const db = await getUnifiedDB();
|
|
81
|
+
return await db.count(STORE_NAME);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Clear all accounts (use with caution!)
|
|
86
|
+
*/
|
|
87
|
+
static async clearAccounts(): Promise<void> {
|
|
88
|
+
const db = await getUnifiedDB();
|
|
89
|
+
await db.clear(STORE_NAME);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getUnifiedDB } from './db';
|
|
2
|
+
import {
|
|
3
|
+
StoreName,
|
|
4
|
+
type ConnectedAppData,
|
|
5
|
+
} from './schema';
|
|
6
|
+
import type { ConnectedApp, AppMetadata } from '@thru/chain-interfaces';
|
|
7
|
+
|
|
8
|
+
const STORE_NAME = StoreName.CONNECTED_APPS;
|
|
9
|
+
|
|
10
|
+
function createKey(accountId: number, appId: string): string {
|
|
11
|
+
return `${accountId}:${appId}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toDomain(record: ConnectedAppData): ConnectedApp {
|
|
15
|
+
return {
|
|
16
|
+
accountId: record.accountId,
|
|
17
|
+
appId: record.appId,
|
|
18
|
+
origin: record.origin,
|
|
19
|
+
metadata: record.metadata,
|
|
20
|
+
connectedAt: record.connectedAt,
|
|
21
|
+
updatedAt: record.updatedAt,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConnectedAppUpsert {
|
|
26
|
+
accountId: number;
|
|
27
|
+
appId: string;
|
|
28
|
+
origin: string;
|
|
29
|
+
metadata: AppMetadata;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Storage helper for connected dApps per account
|
|
34
|
+
*/
|
|
35
|
+
export class ConnectedAppsStorage {
|
|
36
|
+
private static async getDB() {
|
|
37
|
+
return getUnifiedDB();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static async upsert(app: ConnectedAppUpsert): Promise<ConnectedApp> {
|
|
41
|
+
const db = await this.getDB();
|
|
42
|
+
const key = createKey(app.accountId, app.appId);
|
|
43
|
+
const existing = (await db.get(STORE_NAME, key)) as ConnectedAppData | undefined;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
|
|
46
|
+
const record: ConnectedAppData = {
|
|
47
|
+
key,
|
|
48
|
+
accountId: app.accountId,
|
|
49
|
+
appId: app.appId,
|
|
50
|
+
origin: app.origin,
|
|
51
|
+
metadata: app.metadata,
|
|
52
|
+
connectedAt: existing?.connectedAt ?? now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await db.put(STORE_NAME, record);
|
|
57
|
+
return toDomain(record);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static async listByAccount(accountId: number): Promise<ConnectedApp[]> {
|
|
61
|
+
const db = await this.getDB();
|
|
62
|
+
const records = (await db.getAllFromIndex(STORE_NAME, 'by-account', IDBKeyRange.only(accountId))) as ConnectedAppData[];
|
|
63
|
+
return records
|
|
64
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
65
|
+
.map(toDomain);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async remove(accountId: number, appId: string): Promise<void> {
|
|
69
|
+
const db = await this.getDB();
|
|
70
|
+
await db.delete(STORE_NAME, createKey(accountId, appId));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static async clear(): Promise<void> {
|
|
74
|
+
const db = await this.getDB();
|
|
75
|
+
await db.clear(STORE_NAME);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static async get(accountId: number, appId: string): Promise<ConnectedApp | null> {
|
|
79
|
+
const db = await this.getDB();
|
|
80
|
+
const record = (await db.get(STORE_NAME, createKey(accountId, appId))) as ConnectedAppData | undefined;
|
|
81
|
+
return record ? toDomain(record) : null;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { openDB, type IDBPDatabase } from 'idb';
|
|
2
|
+
import { DB_NAME, DB_VERSION, initializeSchema } from './schema';
|
|
3
|
+
|
|
4
|
+
let dbPromise: Promise<IDBPDatabase> | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the shared unified database connection.
|
|
8
|
+
*/
|
|
9
|
+
export function getUnifiedDB(): Promise<IDBPDatabase> {
|
|
10
|
+
if (!dbPromise) {
|
|
11
|
+
dbPromise = openDB(DB_NAME, DB_VERSION, {
|
|
12
|
+
upgrade(db) {
|
|
13
|
+
initializeSchema(db as unknown as IDBDatabase);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return dbPromise;
|
|
18
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { AccountStorage } from './account-store';
|
|
2
|
+
export { ConnectedAppsStorage } from './connected-apps-store';
|
|
3
|
+
export { getUnifiedDB } from './db';
|
|
4
|
+
export {
|
|
5
|
+
type ConnectedAppData,
|
|
6
|
+
StoreName,
|
|
7
|
+
DB_NAME,
|
|
8
|
+
DB_VERSION,
|
|
9
|
+
} from './schema';
|
|
10
|
+
export { type StoredAccount } from './types';
|
|
11
|
+
export {
|
|
12
|
+
loadPasskeyProfiles,
|
|
13
|
+
savePasskeyProfiles,
|
|
14
|
+
createDefaultProfileStore,
|
|
15
|
+
updateProfilePasskey,
|
|
16
|
+
updatePasskeyLastUsed,
|
|
17
|
+
type PasskeyMetadata,
|
|
18
|
+
type PasskeyProfile,
|
|
19
|
+
type PasskeyProfileStore,
|
|
20
|
+
} from './passkey-profiles';
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { getUnifiedDB } from './db';
|
|
2
|
+
import { StoreName } from './schema';
|
|
3
|
+
import type { PasskeyProfileRecord, PasskeyStoreSettings } from './types';
|
|
4
|
+
|
|
5
|
+
const CURRENT_PROFILE_VERSION = 1;
|
|
6
|
+
const SETTINGS_KEY = 'settings';
|
|
7
|
+
|
|
8
|
+
export interface PasskeyMetadata {
|
|
9
|
+
credentialId: string;
|
|
10
|
+
publicKeyX: string;
|
|
11
|
+
publicKeyY: string;
|
|
12
|
+
rpId: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
lastUsedAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PasskeyProfile {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
passkey: PasskeyMetadata | null;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
lastUsedAt: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PasskeyProfileStore {
|
|
27
|
+
profiles: PasskeyProfile[];
|
|
28
|
+
selectedIndex: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load all passkey profiles and settings from IndexedDB.
|
|
33
|
+
* Returns null if no profiles exist or if the DB is unavailable.
|
|
34
|
+
*/
|
|
35
|
+
export async function loadPasskeyProfiles(): Promise<PasskeyProfileStore | null> {
|
|
36
|
+
if (typeof window === 'undefined') return null;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const db = await getUnifiedDB();
|
|
40
|
+
const allRecords = await db.getAll(StoreName.PASSKEY_PROFILES);
|
|
41
|
+
|
|
42
|
+
let settings: PasskeyStoreSettings | null = null;
|
|
43
|
+
const profiles: PasskeyProfile[] = [];
|
|
44
|
+
|
|
45
|
+
for (const record of allRecords) {
|
|
46
|
+
if (record.id === SETTINGS_KEY) {
|
|
47
|
+
settings = record as PasskeyStoreSettings;
|
|
48
|
+
} else if (record.id) {
|
|
49
|
+
profiles.push(record as PasskeyProfile);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (profiles.length === 0) return null;
|
|
54
|
+
|
|
55
|
+
// Run schema migration if needed
|
|
56
|
+
if (settings && settings.version < CURRENT_PROFILE_VERSION) {
|
|
57
|
+
migrateProfiles(profiles, settings.version);
|
|
58
|
+
settings = { ...settings, version: CURRENT_PROFILE_VERSION };
|
|
59
|
+
await db.put(StoreName.PASSKEY_PROFILES, settings);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
profiles,
|
|
64
|
+
selectedIndex: settings?.selectedIndex ?? 0,
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Save all passkey profiles and settings to IndexedDB.
|
|
73
|
+
* Returns true on success, false on failure.
|
|
74
|
+
*/
|
|
75
|
+
export async function savePasskeyProfiles(store: PasskeyProfileStore): Promise<boolean> {
|
|
76
|
+
if (typeof window === 'undefined') return false;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const db = await getUnifiedDB();
|
|
80
|
+
const tx = db.transaction(StoreName.PASSKEY_PROFILES, 'readwrite');
|
|
81
|
+
|
|
82
|
+
// Clear all existing records
|
|
83
|
+
await tx.store.clear();
|
|
84
|
+
|
|
85
|
+
// Write profiles
|
|
86
|
+
for (const profile of store.profiles) {
|
|
87
|
+
await tx.store.put(profile as PasskeyProfileRecord);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Write settings
|
|
91
|
+
const settings: PasskeyStoreSettings = {
|
|
92
|
+
id: SETTINGS_KEY,
|
|
93
|
+
selectedIndex: store.selectedIndex,
|
|
94
|
+
version: CURRENT_PROFILE_VERSION,
|
|
95
|
+
};
|
|
96
|
+
await tx.store.put(settings);
|
|
97
|
+
|
|
98
|
+
await tx.done;
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create a default profile store with one empty profile.
|
|
107
|
+
*/
|
|
108
|
+
export function createDefaultProfileStore(): PasskeyProfileStore {
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
const profileId = typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
111
|
+
? crypto.randomUUID()
|
|
112
|
+
: String(Date.now());
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
profiles: [
|
|
116
|
+
{
|
|
117
|
+
id: profileId,
|
|
118
|
+
label: 'Default Profile',
|
|
119
|
+
passkey: null,
|
|
120
|
+
createdAt: now,
|
|
121
|
+
lastUsedAt: null,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
selectedIndex: 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update a profile's passkey metadata (pure in-memory transform).
|
|
130
|
+
*/
|
|
131
|
+
export function updateProfilePasskey(
|
|
132
|
+
store: PasskeyProfileStore,
|
|
133
|
+
profileIndex: number,
|
|
134
|
+
passkey: PasskeyMetadata
|
|
135
|
+
): PasskeyProfileStore {
|
|
136
|
+
const updatedProfiles = [...store.profiles];
|
|
137
|
+
const current = updatedProfiles[profileIndex];
|
|
138
|
+
if (!current) {
|
|
139
|
+
return store;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
updatedProfiles[profileIndex] = {
|
|
143
|
+
...current,
|
|
144
|
+
passkey,
|
|
145
|
+
lastUsedAt: passkey.lastUsedAt,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
...store,
|
|
150
|
+
profiles: updatedProfiles,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update lastUsedAt timestamp for a profile's passkey (pure in-memory transform).
|
|
156
|
+
*/
|
|
157
|
+
export function updatePasskeyLastUsed(
|
|
158
|
+
store: PasskeyProfileStore,
|
|
159
|
+
profileIndex: number
|
|
160
|
+
): PasskeyProfileStore {
|
|
161
|
+
const updatedProfiles = [...store.profiles];
|
|
162
|
+
const current = updatedProfiles[profileIndex];
|
|
163
|
+
if (!current?.passkey) {
|
|
164
|
+
return store;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const now = new Date().toISOString();
|
|
168
|
+
updatedProfiles[profileIndex] = {
|
|
169
|
+
...current,
|
|
170
|
+
passkey: {
|
|
171
|
+
...current.passkey,
|
|
172
|
+
lastUsedAt: now,
|
|
173
|
+
},
|
|
174
|
+
lastUsedAt: now,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...store,
|
|
179
|
+
profiles: updatedProfiles,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Migrate profile data from an older schema version to current.
|
|
185
|
+
* Mutates profiles in-place for efficiency.
|
|
186
|
+
*/
|
|
187
|
+
function migrateProfiles(_profiles: PasskeyProfile[], _fromVersion: number): void {
|
|
188
|
+
// Currently at version 1, no migrations needed yet.
|
|
189
|
+
// Future migrations would go here:
|
|
190
|
+
// if (fromVersion < 2) { ... }
|
|
191
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AppMetadata } from '@thru/chain-interfaces';
|
|
2
|
+
|
|
3
|
+
export const DB_NAME = 'thru-wallet';
|
|
4
|
+
export const DB_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export enum StoreName {
|
|
7
|
+
CONNECTED_APPS = 'connectedApps',
|
|
8
|
+
ACCOUNTS = 'accounts',
|
|
9
|
+
PASSKEY_PROFILES = 'passkeyProfiles',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConnectedAppData {
|
|
13
|
+
key: string; // `${accountId}:${appId}`
|
|
14
|
+
accountId: number;
|
|
15
|
+
appId: string;
|
|
16
|
+
origin: string;
|
|
17
|
+
metadata: AppMetadata;
|
|
18
|
+
connectedAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize database schema.
|
|
24
|
+
*/
|
|
25
|
+
export function initializeSchema(db: IDBDatabase): void {
|
|
26
|
+
const connectedApps = db.createObjectStore(StoreName.CONNECTED_APPS, { keyPath: 'key' });
|
|
27
|
+
connectedApps.createIndex('by-account', 'accountId', { unique: false });
|
|
28
|
+
connectedApps.createIndex('by-updated', 'updatedAt', { unique: false });
|
|
29
|
+
|
|
30
|
+
const accounts = db.createObjectStore(StoreName.ACCOUNTS, { keyPath: 'index' });
|
|
31
|
+
accounts.createIndex('by-created', 'createdAt', { unique: false });
|
|
32
|
+
|
|
33
|
+
db.createObjectStore(StoreName.PASSKEY_PROFILES, { keyPath: 'id' });
|
|
34
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AddressType } from '@thru/chain-interfaces';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stored account representation in IndexedDB
|
|
5
|
+
*/
|
|
6
|
+
export interface StoredAccount {
|
|
7
|
+
index: number; // BIP44 account index (0, 1, 2, ...)
|
|
8
|
+
label: string; // User-defined name (e.g., "Trading", "NFTs")
|
|
9
|
+
publicKey: string; // Encoded address string (currently base58 SOL; will migrate to Thru)
|
|
10
|
+
path: string; // Full derivation path (m/44'/<coin>'/index'/0')
|
|
11
|
+
createdAt: Date; // Timestamp of account creation
|
|
12
|
+
addressType?: AddressType; // Chain identifier (e.g., 'thru')
|
|
13
|
+
publicKeyRawBase64?: string; // Optional raw 32-byte public key as base64 (for Thru migration)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single passkey profile record stored in IndexedDB.
|
|
18
|
+
*/
|
|
19
|
+
export interface PasskeyProfileRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
passkey: PasskeyMetadataRecord | null;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
lastUsedAt: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Passkey metadata stored alongside a profile.
|
|
29
|
+
*/
|
|
30
|
+
export interface PasskeyMetadataRecord {
|
|
31
|
+
credentialId: string;
|
|
32
|
+
publicKeyX: string;
|
|
33
|
+
publicKeyY: string;
|
|
34
|
+
rpId: string;
|
|
35
|
+
label?: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
lastUsedAt: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Settings singleton stored in the passkeyProfiles store with id='settings'.
|
|
42
|
+
*/
|
|
43
|
+
export interface PasskeyStoreSettings {
|
|
44
|
+
id: 'settings';
|
|
45
|
+
selectedIndex: number;
|
|
46
|
+
version: number;
|
|
47
|
+
}
|
package/tsconfig.json
ADDED