@syncvault/sdk 1.0.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 +161 -0
- package/package.json +49 -0
- package/src/crypto.js +139 -0
- package/src/index.d.ts +60 -0
- package/src/index.js +249 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# @syncvault/sdk
|
|
2
|
+
|
|
3
|
+
Zero-knowledge sync SDK for Node.js and browsers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @syncvault/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start (OAuth Flow - Recommended)
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { SyncVault } from '@syncvault/sdk';
|
|
15
|
+
|
|
16
|
+
const vault = new SyncVault({
|
|
17
|
+
appToken: 'your_app_token',
|
|
18
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
19
|
+
serverUrl: 'https://api.syncvault.io' // optional
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Step 1: Redirect user to authorize
|
|
23
|
+
const authUrl = vault.getAuthUrl();
|
|
24
|
+
window.location.href = authUrl;
|
|
25
|
+
|
|
26
|
+
// Step 2: After redirect, exchange code for token
|
|
27
|
+
// The user will be redirected to: http://localhost:3000/callback?code=xxx
|
|
28
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
29
|
+
const code = urlParams.get('code');
|
|
30
|
+
|
|
31
|
+
// User must provide their encryption password
|
|
32
|
+
const password = prompt('Enter your encryption password:');
|
|
33
|
+
const user = await vault.exchangeCode(code, password);
|
|
34
|
+
|
|
35
|
+
// Step 3: Use the SDK
|
|
36
|
+
await vault.put('data.json', { hello: 'world' });
|
|
37
|
+
const data = await vault.get('data.json');
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start (Direct Auth)
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { SyncVault } from '@syncvault/sdk';
|
|
44
|
+
|
|
45
|
+
const vault = new SyncVault({
|
|
46
|
+
appToken: 'your_app_token',
|
|
47
|
+
serverUrl: 'https://api.syncvault.io' // optional
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Authenticate user directly
|
|
51
|
+
await vault.auth('username', 'password');
|
|
52
|
+
|
|
53
|
+
// Store data (automatically encrypted)
|
|
54
|
+
await vault.put('notes/my-note.json', {
|
|
55
|
+
title: 'Hello',
|
|
56
|
+
content: 'World'
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Retrieve data (automatically decrypted)
|
|
60
|
+
const note = await vault.get('notes/my-note.json');
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API Reference
|
|
64
|
+
|
|
65
|
+
### Constructor
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
new SyncVault({
|
|
69
|
+
appToken: string, // Required: Your app token from developer dashboard
|
|
70
|
+
serverUrl?: string, // Optional: API URL (default: https://api.syncvault.dev)
|
|
71
|
+
redirectUri?: string // Required for OAuth flow
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### OAuth Methods
|
|
76
|
+
|
|
77
|
+
#### `vault.getAuthUrl(state?)`
|
|
78
|
+
Generate OAuth authorization URL. Redirect users here to start the flow.
|
|
79
|
+
|
|
80
|
+
#### `vault.exchangeCode(code, password)`
|
|
81
|
+
Exchange authorization code for access token. Call this after user returns with the code.
|
|
82
|
+
|
|
83
|
+
#### `vault.setAuth(token, password)`
|
|
84
|
+
Manually set authentication state (e.g., from stored session).
|
|
85
|
+
|
|
86
|
+
### Direct Auth Methods
|
|
87
|
+
|
|
88
|
+
#### `vault.auth(username, password)`
|
|
89
|
+
Authenticate user directly. Requires valid app token.
|
|
90
|
+
|
|
91
|
+
#### `vault.register(username, password)`
|
|
92
|
+
Register new user. Requires valid app token.
|
|
93
|
+
|
|
94
|
+
### Data Methods
|
|
95
|
+
|
|
96
|
+
#### `vault.put(path, data)`
|
|
97
|
+
Store encrypted data at the given path.
|
|
98
|
+
|
|
99
|
+
#### `vault.get(path)`
|
|
100
|
+
Retrieve and decrypt data from the given path.
|
|
101
|
+
|
|
102
|
+
#### `vault.list()`
|
|
103
|
+
List all files for this app.
|
|
104
|
+
|
|
105
|
+
#### `vault.delete(path)`
|
|
106
|
+
Delete a file.
|
|
107
|
+
|
|
108
|
+
### Metadata Methods
|
|
109
|
+
|
|
110
|
+
App metadata is unencrypted data stored server-side. Use it for app-specific logic like subscription status, feature flags, or user preferences that don't need encryption.
|
|
111
|
+
|
|
112
|
+
#### `vault.getMetadata()`
|
|
113
|
+
Get app metadata for the current user.
|
|
114
|
+
|
|
115
|
+
#### `vault.setMetadata(metadata)`
|
|
116
|
+
Set app metadata (replaces all existing metadata).
|
|
117
|
+
|
|
118
|
+
#### `vault.updateMetadata(metadata)`
|
|
119
|
+
Update app metadata (merges with existing metadata).
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// Example: Store subscription status
|
|
123
|
+
await vault.setMetadata({
|
|
124
|
+
subscriptionActive: true,
|
|
125
|
+
subscriptionExpiresAt: '2026-12-31',
|
|
126
|
+
plan: 'premium'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Read metadata
|
|
130
|
+
const meta = await vault.getMetadata();
|
|
131
|
+
console.log(meta.subscriptionActive); // true
|
|
132
|
+
|
|
133
|
+
// Update specific fields
|
|
134
|
+
await vault.updateMetadata({ lastLogin: new Date().toISOString() });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### State Methods
|
|
138
|
+
|
|
139
|
+
#### `vault.isAuthenticated()`
|
|
140
|
+
Check if user is authenticated.
|
|
141
|
+
|
|
142
|
+
#### `vault.logout()`
|
|
143
|
+
Clear authentication state.
|
|
144
|
+
|
|
145
|
+
#### `vault.getUser()`
|
|
146
|
+
Get current user info.
|
|
147
|
+
|
|
148
|
+
## App Permissions
|
|
149
|
+
|
|
150
|
+
Apps can request specific permissions when created:
|
|
151
|
+
- `read` - Read user data
|
|
152
|
+
- `write` - Create and update user data
|
|
153
|
+
- `delete` - Delete user data
|
|
154
|
+
|
|
155
|
+
Users see these permissions during OAuth authorization.
|
|
156
|
+
|
|
157
|
+
## Encryption
|
|
158
|
+
|
|
159
|
+
All data is encrypted client-side using AES-256-GCM with a key derived from the user's password using PBKDF2 (100,000 iterations). The server never sees unencrypted data.
|
|
160
|
+
|
|
161
|
+
Note: Metadata is NOT encrypted - use it only for non-sensitive app logic.
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncvault/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SyncVault SDK - Zero-knowledge encrypted sync for Node.js and browsers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"types": "src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"types": "./src/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./crypto": {
|
|
14
|
+
"import": "./src/crypto.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node test/test.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"syncvault",
|
|
25
|
+
"sync",
|
|
26
|
+
"encryption",
|
|
27
|
+
"e2e",
|
|
28
|
+
"end-to-end-encryption",
|
|
29
|
+
"zero-knowledge",
|
|
30
|
+
"aes-256",
|
|
31
|
+
"pbkdf2",
|
|
32
|
+
"cloud-sync",
|
|
33
|
+
"secure-storage"
|
|
34
|
+
],
|
|
35
|
+
"author": "SyncVault <support@syncvault.dev>",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://syncvault.dev",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/syncvault-dev/sdk-js"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/syncvault-dev/sdk-js/issues"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"sideEffects": false
|
|
49
|
+
}
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for SyncVault SDK
|
|
3
|
+
* Uses Web Crypto API (works in Node.js 18+ and browsers)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SALT_LENGTH = 16;
|
|
7
|
+
const IV_LENGTH = 12;
|
|
8
|
+
const KEY_LENGTH = 256;
|
|
9
|
+
const ITERATIONS = 100000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Prepare password for authentication (hashing)
|
|
13
|
+
* This ensures the server never sees the raw password used for encryption
|
|
14
|
+
*/
|
|
15
|
+
export async function prepareAuthPassword(password) {
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
const data = encoder.encode(password);
|
|
18
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
19
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
20
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derive encryption key from password using PBKDF2
|
|
25
|
+
*/
|
|
26
|
+
export async function deriveKey(password, salt) {
|
|
27
|
+
const encoder = new TextEncoder();
|
|
28
|
+
const passwordBuffer = encoder.encode(password);
|
|
29
|
+
|
|
30
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
31
|
+
'raw',
|
|
32
|
+
passwordBuffer,
|
|
33
|
+
'PBKDF2',
|
|
34
|
+
false,
|
|
35
|
+
['deriveKey']
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return crypto.subtle.deriveKey(
|
|
39
|
+
{
|
|
40
|
+
name: 'PBKDF2',
|
|
41
|
+
salt,
|
|
42
|
+
iterations: ITERATIONS,
|
|
43
|
+
hash: 'SHA-256'
|
|
44
|
+
},
|
|
45
|
+
keyMaterial,
|
|
46
|
+
{ name: 'AES-GCM', length: KEY_LENGTH },
|
|
47
|
+
false,
|
|
48
|
+
['encrypt', 'decrypt']
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a random salt
|
|
54
|
+
*/
|
|
55
|
+
export function generateSalt() {
|
|
56
|
+
return crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Encrypt data using AES-256-GCM
|
|
61
|
+
* Returns: salt (16 bytes) + iv (12 bytes) + ciphertext
|
|
62
|
+
*/
|
|
63
|
+
export async function encrypt(data, password) {
|
|
64
|
+
const encoder = new TextEncoder();
|
|
65
|
+
const dataBuffer = encoder.encode(JSON.stringify(data));
|
|
66
|
+
|
|
67
|
+
const salt = generateSalt();
|
|
68
|
+
const key = await deriveKey(password, salt);
|
|
69
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
70
|
+
|
|
71
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
72
|
+
{ name: 'AES-GCM', iv },
|
|
73
|
+
key,
|
|
74
|
+
dataBuffer
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Combine salt + iv + ciphertext
|
|
78
|
+
const combined = new Uint8Array(
|
|
79
|
+
SALT_LENGTH + IV_LENGTH + ciphertext.byteLength
|
|
80
|
+
);
|
|
81
|
+
combined.set(salt, 0);
|
|
82
|
+
combined.set(iv, SALT_LENGTH);
|
|
83
|
+
combined.set(new Uint8Array(ciphertext), SALT_LENGTH + IV_LENGTH);
|
|
84
|
+
|
|
85
|
+
return bufferToBase64(combined);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Decrypt data using AES-256-GCM
|
|
90
|
+
*/
|
|
91
|
+
export async function decrypt(encryptedBase64, password) {
|
|
92
|
+
const combined = base64ToBuffer(encryptedBase64);
|
|
93
|
+
|
|
94
|
+
const salt = combined.slice(0, SALT_LENGTH);
|
|
95
|
+
const iv = combined.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
96
|
+
const ciphertext = combined.slice(SALT_LENGTH + IV_LENGTH);
|
|
97
|
+
|
|
98
|
+
const key = await deriveKey(password, salt);
|
|
99
|
+
|
|
100
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
101
|
+
{ name: 'AES-GCM', iv },
|
|
102
|
+
key,
|
|
103
|
+
ciphertext
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const decoder = new TextDecoder();
|
|
107
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert Uint8Array to base64 string
|
|
112
|
+
*/
|
|
113
|
+
function bufferToBase64(buffer) {
|
|
114
|
+
if (typeof Buffer !== 'undefined') {
|
|
115
|
+
return Buffer.from(buffer).toString('base64');
|
|
116
|
+
}
|
|
117
|
+
// Browser fallback
|
|
118
|
+
let binary = '';
|
|
119
|
+
for (let i = 0; i < buffer.byteLength; i++) {
|
|
120
|
+
binary += String.fromCharCode(buffer[i]);
|
|
121
|
+
}
|
|
122
|
+
return btoa(binary);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert base64 string to Uint8Array
|
|
127
|
+
*/
|
|
128
|
+
function base64ToBuffer(base64) {
|
|
129
|
+
if (typeof Buffer !== 'undefined') {
|
|
130
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
131
|
+
}
|
|
132
|
+
// Browser fallback
|
|
133
|
+
const binary = atob(base64);
|
|
134
|
+
const bytes = new Uint8Array(binary.length);
|
|
135
|
+
for (let i = 0; i < binary.length; i++) {
|
|
136
|
+
bytes[i] = binary.charCodeAt(i);
|
|
137
|
+
}
|
|
138
|
+
return bytes;
|
|
139
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface SyncVaultOptions {
|
|
2
|
+
appToken: string;
|
|
3
|
+
serverUrl?: string;
|
|
4
|
+
redirectUri?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface User {
|
|
8
|
+
id: string;
|
|
9
|
+
username: string;
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FileInfo {
|
|
14
|
+
path: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PutResponse {
|
|
19
|
+
path: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeleteResponse {
|
|
24
|
+
deleted: boolean;
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Metadata = Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
export declare class SyncVault {
|
|
31
|
+
constructor(options: SyncVaultOptions);
|
|
32
|
+
|
|
33
|
+
// OAuth flow
|
|
34
|
+
getAuthUrl(state?: string): string;
|
|
35
|
+
exchangeCode(code: string, password: string): Promise<User>;
|
|
36
|
+
setAuth(token: string, password: string): void;
|
|
37
|
+
|
|
38
|
+
// Direct auth (requires valid app token)
|
|
39
|
+
auth(username: string, password: string): Promise<User>;
|
|
40
|
+
register(username: string, password: string): Promise<User>;
|
|
41
|
+
|
|
42
|
+
// Data operations
|
|
43
|
+
put<T = unknown>(path: string, data: T): Promise<PutResponse>;
|
|
44
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
45
|
+
list(): Promise<FileInfo[]>;
|
|
46
|
+
delete(path: string): Promise<DeleteResponse>;
|
|
47
|
+
|
|
48
|
+
// Metadata operations (unencrypted server-side data)
|
|
49
|
+
getMetadata<T extends Metadata = Metadata>(): Promise<T>;
|
|
50
|
+
setMetadata<T extends Metadata = Metadata>(metadata: T): Promise<T>;
|
|
51
|
+
updateMetadata<T extends Metadata = Metadata>(metadata: Partial<T>): Promise<T>;
|
|
52
|
+
|
|
53
|
+
// State
|
|
54
|
+
isAuthenticated(): boolean;
|
|
55
|
+
logout(): void;
|
|
56
|
+
getUser(): Promise<User>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export declare function encrypt(data: unknown, password: string): Promise<string>;
|
|
60
|
+
export declare function decrypt<T = unknown>(encryptedBase64: string, password: string): Promise<T>;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { encrypt, decrypt, prepareAuthPassword } from './crypto.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SERVER = 'https://api.syncvault.dev';
|
|
4
|
+
|
|
5
|
+
export class SyncVault {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
if (!options.appToken) {
|
|
8
|
+
throw new Error('appToken is required');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
this.appToken = options.appToken;
|
|
12
|
+
this.serverUrl = options.serverUrl || DEFAULT_SERVER;
|
|
13
|
+
this.redirectUri = options.redirectUri || null;
|
|
14
|
+
this.token = null;
|
|
15
|
+
this.password = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate OAuth authorization URL
|
|
20
|
+
* Redirect users to this URL to start the OAuth flow
|
|
21
|
+
*/
|
|
22
|
+
getAuthUrl(state = null) {
|
|
23
|
+
if (!this.redirectUri) {
|
|
24
|
+
throw new Error('redirectUri is required for OAuth flow');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
app_token: this.appToken,
|
|
29
|
+
redirect_uri: this.redirectUri
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (state) {
|
|
33
|
+
params.set('state', state);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${this.serverUrl}/api/oauth/authorize?${params}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Exchange authorization code for access token (OAuth flow)
|
|
41
|
+
* Call this after user is redirected back with the code
|
|
42
|
+
*/
|
|
43
|
+
async exchangeCode(code, password) {
|
|
44
|
+
const response = await this._request('/api/oauth/token', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
code,
|
|
48
|
+
app_token: this.appToken,
|
|
49
|
+
redirect_uri: this.redirectUri
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.token = response.access_token;
|
|
54
|
+
this.password = password;
|
|
55
|
+
|
|
56
|
+
return response.user;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Authenticate user with SyncVault (direct auth - requires valid app token)
|
|
61
|
+
*/
|
|
62
|
+
async auth(username, password) {
|
|
63
|
+
const authPassword = await prepareAuthPassword(password);
|
|
64
|
+
const response = await this._request('/api/user/auth/login', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: JSON.stringify({ username, password: authPassword })
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.token = response.token;
|
|
70
|
+
this.password = password;
|
|
71
|
+
|
|
72
|
+
return response.user;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register a new user (direct auth - requires valid app token)
|
|
77
|
+
*/
|
|
78
|
+
async register(username, password) {
|
|
79
|
+
const authPassword = await prepareAuthPassword(password);
|
|
80
|
+
const response = await this._request('/api/user/auth/register', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: JSON.stringify({ username, password: authPassword })
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
this.token = response.token;
|
|
86
|
+
this.password = password;
|
|
87
|
+
|
|
88
|
+
return response.user;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Set authentication state manually (e.g., from stored session)
|
|
93
|
+
*/
|
|
94
|
+
setAuth(token, password) {
|
|
95
|
+
this.token = token;
|
|
96
|
+
this.password = password;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Store encrypted data
|
|
101
|
+
*/
|
|
102
|
+
async put(path, data) {
|
|
103
|
+
this._checkAuth();
|
|
104
|
+
|
|
105
|
+
const encrypted = await encrypt(data, this.password);
|
|
106
|
+
|
|
107
|
+
const response = await this._request('/api/sync/put', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: JSON.stringify({ path, data: encrypted })
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Retrieve and decrypt data
|
|
117
|
+
*/
|
|
118
|
+
async get(path) {
|
|
119
|
+
this._checkAuth();
|
|
120
|
+
|
|
121
|
+
const response = await this._request(`/api/sync/get?path=${encodeURIComponent(path)}`);
|
|
122
|
+
|
|
123
|
+
return decrypt(response.data, this.password);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* List all files
|
|
128
|
+
*/
|
|
129
|
+
async list() {
|
|
130
|
+
this._checkAuth();
|
|
131
|
+
|
|
132
|
+
const response = await this._request('/api/sync/list');
|
|
133
|
+
|
|
134
|
+
return response.files;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete a file
|
|
139
|
+
*/
|
|
140
|
+
async delete(path) {
|
|
141
|
+
this._checkAuth();
|
|
142
|
+
|
|
143
|
+
const response = await this._request('/api/sync/delete', {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: JSON.stringify({ path })
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return response;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get app metadata for current user (unencrypted, server-side data)
|
|
153
|
+
*/
|
|
154
|
+
async getMetadata() {
|
|
155
|
+
this._checkAuth();
|
|
156
|
+
|
|
157
|
+
const response = await this._request('/api/sync/metadata');
|
|
158
|
+
return response.metadata;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Set app metadata for current user (replaces all metadata)
|
|
163
|
+
*/
|
|
164
|
+
async setMetadata(metadata) {
|
|
165
|
+
this._checkAuth();
|
|
166
|
+
|
|
167
|
+
const response = await this._request('/api/sync/metadata', {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
body: JSON.stringify({ metadata })
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return response.metadata;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Update app metadata for current user (merges with existing)
|
|
177
|
+
*/
|
|
178
|
+
async updateMetadata(metadata) {
|
|
179
|
+
this._checkAuth();
|
|
180
|
+
|
|
181
|
+
const response = await this._request('/api/sync/metadata', {
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
body: JSON.stringify({ metadata })
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return response.metadata;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if user is authenticated
|
|
191
|
+
*/
|
|
192
|
+
isAuthenticated() {
|
|
193
|
+
return this.token !== null && this.password !== null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clear authentication state
|
|
198
|
+
*/
|
|
199
|
+
logout() {
|
|
200
|
+
this.token = null;
|
|
201
|
+
this.password = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get current user info
|
|
206
|
+
*/
|
|
207
|
+
async getUser() {
|
|
208
|
+
this._checkAuth();
|
|
209
|
+
|
|
210
|
+
return this._request('/api/user/auth/me');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_checkAuth() {
|
|
214
|
+
if (!this.token || !this.password) {
|
|
215
|
+
throw new Error('Not authenticated. Call auth() or register() first.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async _request(path, options = {}) {
|
|
220
|
+
const url = `${this.serverUrl}${path}`;
|
|
221
|
+
|
|
222
|
+
const headers = {
|
|
223
|
+
'Content-Type': 'application/json',
|
|
224
|
+
'X-App-Token': this.appToken
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (this.token) {
|
|
228
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const response = await fetch(url, {
|
|
232
|
+
...options,
|
|
233
|
+
headers: {
|
|
234
|
+
...headers,
|
|
235
|
+
...options.headers
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const data = await response.json();
|
|
240
|
+
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
throw new Error(data.error || 'Request failed');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return data;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export { encrypt, decrypt } from './crypto.js';
|