@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 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';