@sparkvault/sdk-mobile 0.1.3
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 +186 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +515 -0
- package/dist/auth.js.map +1 -0
- package/dist/billing.d.ts +21 -0
- package/dist/billing.js +10 -0
- package/dist/billing.js.map +1 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +36 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +69 -0
- package/dist/config.js.map +1 -0
- package/dist/encoding.d.ts +7 -0
- package/dist/encoding.js +113 -0
- package/dist/encoding.js.map +1 -0
- package/dist/entropy.d.ts +18 -0
- package/dist/entropy.js +35 -0
- package/dist/entropy.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +72 -0
- package/dist/errors.js.map +1 -0
- package/dist/folders.d.ts +15 -0
- package/dist/folders.js +54 -0
- package/dist/folders.js.map +1 -0
- package/dist/health.d.ts +19 -0
- package/dist/health.js +60 -0
- package/dist/health.js.map +1 -0
- package/dist/http.d.ts +45 -0
- package/dist/http.js +351 -0
- package/dist/http.js.map +1 -0
- package/dist/identity-dialog.d.ts +19 -0
- package/dist/identity-dialog.js +656 -0
- package/dist/identity-dialog.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/ingots.d.ts +99 -0
- package/dist/ingots.js +365 -0
- package/dist/ingots.js.map +1 -0
- package/dist/mutex.d.ts +6 -0
- package/dist/mutex.js +20 -0
- package/dist/mutex.js.map +1 -0
- package/dist/push-tokens.d.ts +12 -0
- package/dist/push-tokens.js +15 -0
- package/dist/push-tokens.js.map +1 -0
- package/dist/sparks.d.ts +39 -0
- package/dist/sparks.js +32 -0
- package/dist/sparks.js.map +1 -0
- package/dist/tus.d.ts +24 -0
- package/dist/tus.js +202 -0
- package/dist/tus.js.map +1 -0
- package/dist/types.d.ts +406 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.js +45 -0
- package/dist/validation.js.map +1 -0
- package/dist/vaults.d.ts +94 -0
- package/dist/vaults.js +106 -0
- package/dist/vaults.js.map +1 -0
- package/package.json +58 -0
- package/src/auth.ts +707 -0
- package/src/billing.ts +30 -0
- package/src/client.ts +51 -0
- package/src/config.ts +123 -0
- package/src/encoding.ts +150 -0
- package/src/entropy.ts +56 -0
- package/src/errors.ts +110 -0
- package/src/folders.ts +76 -0
- package/src/health.ts +81 -0
- package/src/http.ts +429 -0
- package/src/identity-dialog.tsx +955 -0
- package/src/index.ts +103 -0
- package/src/ingots.ts +593 -0
- package/src/mutex.ts +26 -0
- package/src/push-tokens.ts +26 -0
- package/src/sparks.ts +73 -0
- package/src/tus.ts +280 -0
- package/src/types.ts +487 -0
- package/src/validation.ts +49 -0
- package/src/vaults.ts +271 -0
package/src/billing.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MobileHttpClient } from './http.js';
|
|
2
|
+
|
|
3
|
+
export interface BillingBalance {
|
|
4
|
+
balance: string;
|
|
5
|
+
balance_usd?: number;
|
|
6
|
+
currency?: string;
|
|
7
|
+
monthly_spend?: string;
|
|
8
|
+
monthly_spend_usd?: number;
|
|
9
|
+
transaction_count?: number;
|
|
10
|
+
recent_transactions?: Array<{
|
|
11
|
+
type: string;
|
|
12
|
+
amount: string;
|
|
13
|
+
balance_after?: string;
|
|
14
|
+
reference_id?: string;
|
|
15
|
+
created_at: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class MobileBillingClient {
|
|
20
|
+
private readonly http: MobileHttpClient;
|
|
21
|
+
|
|
22
|
+
constructor(http: MobileHttpClient) {
|
|
23
|
+
this.http = http;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getBalance(): Promise<BillingBalance> {
|
|
27
|
+
const response = await this.http.get<BillingBalance>('/billing/balance');
|
|
28
|
+
return response.data;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { MobileAuthClient } from './auth.js';
|
|
2
|
+
import { MobileBillingClient } from './billing.js';
|
|
3
|
+
import { resolveMobileConfig, type SparkVaultMobileConfig, type ResolvedMobileConfig } from './config.js';
|
|
4
|
+
import { MobileEntropyClient } from './entropy.js';
|
|
5
|
+
import { MobileFoldersClient } from './folders.js';
|
|
6
|
+
import { MobileHealthClient } from './health.js';
|
|
7
|
+
import { MobileHttpClient, type AuthEventListener } from './http.js';
|
|
8
|
+
import { MobileIngotsClient } from './ingots.js';
|
|
9
|
+
import { MobilePushTokensClient } from './push-tokens.js';
|
|
10
|
+
import { MobileSparksClient } from './sparks.js';
|
|
11
|
+
import { MobileVaultsClient } from './vaults.js';
|
|
12
|
+
|
|
13
|
+
export class SparkVaultMobile {
|
|
14
|
+
readonly config: ResolvedMobileConfig;
|
|
15
|
+
readonly auth: MobileAuthClient;
|
|
16
|
+
readonly vaults: MobileVaultsClient;
|
|
17
|
+
readonly ingots: MobileIngotsClient;
|
|
18
|
+
readonly folders: MobileFoldersClient;
|
|
19
|
+
readonly health: MobileHealthClient;
|
|
20
|
+
readonly sparks: MobileSparksClient;
|
|
21
|
+
readonly entropy: MobileEntropyClient;
|
|
22
|
+
readonly pushTokens: MobilePushTokensClient;
|
|
23
|
+
readonly billing: MobileBillingClient;
|
|
24
|
+
private readonly http: MobileHttpClient;
|
|
25
|
+
|
|
26
|
+
constructor(config: SparkVaultMobileConfig) {
|
|
27
|
+
this.config = resolveMobileConfig(config);
|
|
28
|
+
this.http = new MobileHttpClient(this.config);
|
|
29
|
+
this.auth = new MobileAuthClient(this.config, this.http);
|
|
30
|
+
this.vaults = new MobileVaultsClient(this.http);
|
|
31
|
+
this.ingots = new MobileIngotsClient(this.config, this.http);
|
|
32
|
+
this.folders = new MobileFoldersClient(this.http);
|
|
33
|
+
this.health = new MobileHealthClient(this.config);
|
|
34
|
+
this.sparks = new MobileSparksClient(this.http);
|
|
35
|
+
this.entropy = new MobileEntropyClient(this.http);
|
|
36
|
+
this.pushTokens = new MobilePushTokensClient(this.http);
|
|
37
|
+
this.billing = new MobileBillingClient(this.http);
|
|
38
|
+
|
|
39
|
+
if (this.config.preloadIdentityConfig) {
|
|
40
|
+
this.auth.preloadConfig();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onAuthEvent(listener: AuthEventListener): () => void {
|
|
45
|
+
return this.http.onAuthEvent(listener);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createSparkVaultMobileClient(config: SparkVaultMobileConfig): SparkVaultMobile {
|
|
50
|
+
return new SparkVaultMobile(config);
|
|
51
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { SparkVaultValidationError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
FetchLike,
|
|
4
|
+
MobileFileDownloader,
|
|
5
|
+
MobileFileReader,
|
|
6
|
+
SparkVaultLogger,
|
|
7
|
+
TokenStorageAdapter,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_API_BASE_URL = 'https://api.sparkvault.com/v1';
|
|
11
|
+
const DEFAULT_IDENTITY_BASE_URL = 'https://api.sparkvault.com/v1/apps/identity';
|
|
12
|
+
|
|
13
|
+
// Hosts that are allowed to serve backend-issued ingot download URLs.
|
|
14
|
+
// Keep in sync with the sdk-js default in packages/sdk-js/src/config.ts.
|
|
15
|
+
const DEFAULT_ALLOWED_DOWNLOAD_HOST_PATTERNS: RegExp[] = [
|
|
16
|
+
/(^|\.)sparkvault\.com$/i,
|
|
17
|
+
/(^|\.)(x|files|file|send|spark|by|at|db|auth)\.sv$/i,
|
|
18
|
+
/\.amazonaws\.com$/i,
|
|
19
|
+
/\.cloudfront\.net$/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const noopLogger: SparkVaultLogger = {
|
|
23
|
+
debug: () => undefined,
|
|
24
|
+
info: () => undefined,
|
|
25
|
+
warn: () => undefined,
|
|
26
|
+
error: () => undefined,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export interface SparkVaultMobileConfig {
|
|
30
|
+
tokenStorage: TokenStorageAdapter;
|
|
31
|
+
apiBaseUrl?: string;
|
|
32
|
+
identityBaseUrl?: string;
|
|
33
|
+
identityAccountId: string;
|
|
34
|
+
preloadIdentityConfig?: boolean;
|
|
35
|
+
userAgent?: string;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
fileTransferTimeoutMs?: number;
|
|
38
|
+
tusPostTimeoutMs?: number;
|
|
39
|
+
tusChunkTimeoutMs?: number;
|
|
40
|
+
tusChunkSizeBytes?: number;
|
|
41
|
+
fetch?: FetchLike;
|
|
42
|
+
fileReader?: MobileFileReader;
|
|
43
|
+
fileDownloader?: MobileFileDownloader;
|
|
44
|
+
logger?: Partial<SparkVaultLogger>;
|
|
45
|
+
allowedDownloadHostPatterns?: RegExp[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolvedMobileConfig {
|
|
49
|
+
tokenStorage: TokenStorageAdapter;
|
|
50
|
+
apiBaseUrl: string;
|
|
51
|
+
identityBaseUrl: string;
|
|
52
|
+
identityAccountId: string;
|
|
53
|
+
preloadIdentityConfig: boolean;
|
|
54
|
+
userAgent: string;
|
|
55
|
+
timeoutMs: number;
|
|
56
|
+
fileTransferTimeoutMs: number;
|
|
57
|
+
tusPostTimeoutMs: number;
|
|
58
|
+
tusChunkTimeoutMs: number;
|
|
59
|
+
tusChunkSizeBytes: number;
|
|
60
|
+
fetch: FetchLike;
|
|
61
|
+
fileReader?: MobileFileReader;
|
|
62
|
+
fileDownloader?: MobileFileDownloader;
|
|
63
|
+
logger: SparkVaultLogger;
|
|
64
|
+
allowedDownloadHostPatterns: RegExp[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeBaseUrl(url: string, field: string): string {
|
|
68
|
+
if (!url || typeof url !== 'string') {
|
|
69
|
+
throw new SparkVaultValidationError(`${field} must be a non-empty string`, { field });
|
|
70
|
+
}
|
|
71
|
+
return url.replace(/\/+$/, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function defaultFetch(url: string, options?: RequestInit): Promise<Response> {
|
|
75
|
+
if (typeof fetch !== 'function') {
|
|
76
|
+
throw new SparkVaultValidationError('A fetch implementation is required in this runtime');
|
|
77
|
+
}
|
|
78
|
+
return fetch(url, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveMobileConfig(config: SparkVaultMobileConfig): ResolvedMobileConfig {
|
|
82
|
+
if (!config.tokenStorage) {
|
|
83
|
+
throw new SparkVaultValidationError('tokenStorage is required', { field: 'tokenStorage' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const identityAccountId = config.identityAccountId;
|
|
87
|
+
if (!identityAccountId || typeof identityAccountId !== 'string') {
|
|
88
|
+
throw new SparkVaultValidationError('identityAccountId is required', {
|
|
89
|
+
field: 'identityAccountId',
|
|
90
|
+
received: identityAccountId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!identityAccountId.startsWith('acc_')) {
|
|
95
|
+
throw new SparkVaultValidationError('identityAccountId must start with "acc_"', {
|
|
96
|
+
field: 'identityAccountId',
|
|
97
|
+
received: identityAccountId,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
tokenStorage: config.tokenStorage,
|
|
103
|
+
apiBaseUrl: normalizeBaseUrl(config.apiBaseUrl ?? DEFAULT_API_BASE_URL, 'apiBaseUrl'),
|
|
104
|
+
identityBaseUrl: normalizeBaseUrl(config.identityBaseUrl ?? DEFAULT_IDENTITY_BASE_URL, 'identityBaseUrl'),
|
|
105
|
+
identityAccountId,
|
|
106
|
+
preloadIdentityConfig: config.preloadIdentityConfig ?? true,
|
|
107
|
+
userAgent: config.userAgent ?? 'SparkVaultMobileSDK/0.1.0',
|
|
108
|
+
timeoutMs: config.timeoutMs ?? 30000,
|
|
109
|
+
fileTransferTimeoutMs: config.fileTransferTimeoutMs ?? 300000,
|
|
110
|
+
tusPostTimeoutMs: config.tusPostTimeoutMs ?? 30000,
|
|
111
|
+
tusChunkTimeoutMs: config.tusChunkTimeoutMs ?? 120000,
|
|
112
|
+
tusChunkSizeBytes: config.tusChunkSizeBytes ?? 5 * 1024 * 1024,
|
|
113
|
+
fetch: config.fetch ?? defaultFetch,
|
|
114
|
+
fileReader: config.fileReader,
|
|
115
|
+
fileDownloader: config.fileDownloader,
|
|
116
|
+
logger: { ...noopLogger, ...config.logger },
|
|
117
|
+
allowedDownloadHostPatterns: config.allowedDownloadHostPatterns ?? DEFAULT_ALLOWED_DOWNLOAD_HOST_PATTERNS,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function identityUrl(config: ResolvedMobileConfig, path: string): string {
|
|
122
|
+
return `${config.identityBaseUrl}/${config.identityAccountId}${path}`;
|
|
123
|
+
}
|
package/src/encoding.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
2
|
+
const BASE64_LOOKUP = new Map<string, number>(
|
|
3
|
+
Array.from(BASE64_ALPHABET).map((char, index) => [char, index])
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
export function utf8ToBytes(value: string): Uint8Array {
|
|
7
|
+
const bytes: number[] = [];
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < value.length; i++) {
|
|
10
|
+
let codePoint = value.charCodeAt(i);
|
|
11
|
+
|
|
12
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdbff && i + 1 < value.length) {
|
|
13
|
+
const next = value.charCodeAt(i + 1);
|
|
14
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
15
|
+
codePoint = 0x10000 + ((codePoint - 0xd800) << 10) + (next - 0xdc00);
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (codePoint <= 0x7f) {
|
|
21
|
+
bytes.push(codePoint);
|
|
22
|
+
} else if (codePoint <= 0x7ff) {
|
|
23
|
+
bytes.push(0xc0 | (codePoint >> 6), 0x80 | (codePoint & 0x3f));
|
|
24
|
+
} else if (codePoint <= 0xffff) {
|
|
25
|
+
bytes.push(
|
|
26
|
+
0xe0 | (codePoint >> 12),
|
|
27
|
+
0x80 | ((codePoint >> 6) & 0x3f),
|
|
28
|
+
0x80 | (codePoint & 0x3f)
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
bytes.push(
|
|
32
|
+
0xf0 | (codePoint >> 18),
|
|
33
|
+
0x80 | ((codePoint >> 12) & 0x3f),
|
|
34
|
+
0x80 | ((codePoint >> 6) & 0x3f),
|
|
35
|
+
0x80 | (codePoint & 0x3f)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Uint8Array(bytes);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function base64EncodeUtf8(value: string): string {
|
|
44
|
+
return bytesToBase64(utf8ToBytes(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function bytesToUtf8(bytes: Uint8Array): string {
|
|
48
|
+
let output = '';
|
|
49
|
+
let i = 0;
|
|
50
|
+
|
|
51
|
+
while (i < bytes.length) {
|
|
52
|
+
const byte1 = bytes[i++];
|
|
53
|
+
if (byte1 < 0x80) {
|
|
54
|
+
output += String.fromCharCode(byte1);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (byte1 >= 0xc0 && byte1 < 0xe0) {
|
|
59
|
+
const byte2 = bytes[i++];
|
|
60
|
+
output += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (byte1 >= 0xe0 && byte1 < 0xf0) {
|
|
65
|
+
const byte2 = bytes[i++];
|
|
66
|
+
const byte3 = bytes[i++];
|
|
67
|
+
output += String.fromCharCode(
|
|
68
|
+
((byte1 & 0x0f) << 12) |
|
|
69
|
+
((byte2 & 0x3f) << 6) |
|
|
70
|
+
(byte3 & 0x3f)
|
|
71
|
+
);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const byte2 = bytes[i++];
|
|
76
|
+
const byte3 = bytes[i++];
|
|
77
|
+
const byte4 = bytes[i++];
|
|
78
|
+
const codePoint = (
|
|
79
|
+
((byte1 & 0x07) << 18) |
|
|
80
|
+
((byte2 & 0x3f) << 12) |
|
|
81
|
+
((byte3 & 0x3f) << 6) |
|
|
82
|
+
(byte4 & 0x3f)
|
|
83
|
+
) - 0x10000;
|
|
84
|
+
output += String.fromCharCode(
|
|
85
|
+
0xd800 + (codePoint >> 10),
|
|
86
|
+
0xdc00 + (codePoint & 0x3ff)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return output;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function base64UrlToBytes(value: string): Uint8Array {
|
|
94
|
+
return base64ToBytes(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function base64UrlToUtf8(value: string): string {
|
|
98
|
+
return bytesToUtf8(base64UrlToBytes(value));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
102
|
+
let output = '';
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
105
|
+
const byte1 = bytes[i];
|
|
106
|
+
const byte2 = bytes[i + 1] ?? 0;
|
|
107
|
+
const byte3 = bytes[i + 2] ?? 0;
|
|
108
|
+
const triplet = (byte1 << 16) | (byte2 << 8) | byte3;
|
|
109
|
+
|
|
110
|
+
output += BASE64_ALPHABET[(triplet >> 18) & 0x3f];
|
|
111
|
+
output += BASE64_ALPHABET[(triplet >> 12) & 0x3f];
|
|
112
|
+
output += i + 1 < bytes.length ? BASE64_ALPHABET[(triplet >> 6) & 0x3f] : '=';
|
|
113
|
+
output += i + 2 < bytes.length ? BASE64_ALPHABET[triplet & 0x3f] : '=';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return output;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function base64ToBytes(value: string): Uint8Array {
|
|
120
|
+
const sanitized = value.replace(/\s/g, '').replace(/-/g, '+').replace(/_/g, '/');
|
|
121
|
+
if (sanitized.length % 4 === 1) {
|
|
122
|
+
throw new Error('Invalid base64 input');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const padded = sanitized.padEnd(Math.ceil(sanitized.length / 4) * 4, '=');
|
|
126
|
+
const bytes: number[] = [];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < padded.length; i += 4) {
|
|
129
|
+
const c1 = padded[i];
|
|
130
|
+
const c2 = padded[i + 1];
|
|
131
|
+
const c3 = padded[i + 2];
|
|
132
|
+
const c4 = padded[i + 3];
|
|
133
|
+
|
|
134
|
+
const v1 = BASE64_LOOKUP.get(c1);
|
|
135
|
+
const v2 = BASE64_LOOKUP.get(c2);
|
|
136
|
+
const v3 = c3 === '=' ? 0 : BASE64_LOOKUP.get(c3);
|
|
137
|
+
const v4 = c4 === '=' ? 0 : BASE64_LOOKUP.get(c4);
|
|
138
|
+
|
|
139
|
+
if (v1 === undefined || v2 === undefined || v3 === undefined || v4 === undefined) {
|
|
140
|
+
throw new Error('Invalid base64 input');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const triplet = (v1 << 18) | (v2 << 12) | (v3 << 6) | v4;
|
|
144
|
+
bytes.push((triplet >> 16) & 0xff);
|
|
145
|
+
if (c3 !== '=') bytes.push((triplet >> 8) & 0xff);
|
|
146
|
+
if (c4 !== '=') bytes.push(triplet & 0xff);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return new Uint8Array(bytes);
|
|
150
|
+
}
|
package/src/entropy.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { base64ToBytes } from './encoding.js';
|
|
2
|
+
import { SparkVaultMobileError, SparkVaultValidationError } from './errors.js';
|
|
3
|
+
import type { MobileHttpClient } from './http.js';
|
|
4
|
+
|
|
5
|
+
export type EntropyFormat = 'hex' | 'base64' | 'bytes' | 'uuid';
|
|
6
|
+
|
|
7
|
+
export interface GenerateEntropyOptions {
|
|
8
|
+
bytes: number;
|
|
9
|
+
format?: EntropyFormat;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EntropyResponse {
|
|
13
|
+
value: string;
|
|
14
|
+
num_bytes: number;
|
|
15
|
+
format?: EntropyFormat;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class MobileEntropyClient {
|
|
19
|
+
private readonly http: MobileHttpClient;
|
|
20
|
+
|
|
21
|
+
constructor(http: MobileHttpClient) {
|
|
22
|
+
this.http = http;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async generate(options: GenerateEntropyOptions): Promise<EntropyResponse> {
|
|
26
|
+
this.validateByteCount(options.bytes);
|
|
27
|
+
const response = await this.http.post<EntropyResponse>('/entropy/generate', {
|
|
28
|
+
num_bytes: options.bytes,
|
|
29
|
+
format: options.format ?? 'base64',
|
|
30
|
+
});
|
|
31
|
+
return response.data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async generateBytes(bytes: number): Promise<Uint8Array> {
|
|
35
|
+
const response = await this.generate({ bytes, format: 'base64' });
|
|
36
|
+
if (typeof response.value !== 'string' || response.value.length === 0) {
|
|
37
|
+
throw new SparkVaultMobileError('Entropy endpoint returned no value');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const entropy = base64ToBytes(response.value);
|
|
41
|
+
if (entropy.length !== bytes) {
|
|
42
|
+
throw new SparkVaultMobileError(`Entropy length mismatch: expected ${bytes}, got ${entropy.length}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return entropy;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private validateByteCount(bytes: number): void {
|
|
49
|
+
if (!Number.isInteger(bytes) || bytes < 1 || bytes > 1024) {
|
|
50
|
+
throw new SparkVaultValidationError('Entropy byte count must be an integer from 1 to 1024', {
|
|
51
|
+
field: 'bytes',
|
|
52
|
+
received: bytes,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ApiMeta } from './types.js';
|
|
2
|
+
|
|
3
|
+
export class SparkVaultMobileError extends Error {
|
|
4
|
+
readonly code: string | number;
|
|
5
|
+
readonly statusCode?: number;
|
|
6
|
+
readonly details?: Record<string, unknown>;
|
|
7
|
+
readonly meta?: ApiMeta;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
options: {
|
|
12
|
+
code?: string | number;
|
|
13
|
+
statusCode?: number;
|
|
14
|
+
details?: Record<string, unknown>;
|
|
15
|
+
meta?: ApiMeta;
|
|
16
|
+
} = {}
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'SparkVaultMobileError';
|
|
20
|
+
this.code = options.code ?? 'sparkvault_error';
|
|
21
|
+
this.statusCode = options.statusCode;
|
|
22
|
+
this.details = options.details;
|
|
23
|
+
this.meta = options.meta;
|
|
24
|
+
Object.setPrototypeOf(this, SparkVaultMobileError.prototype);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SparkVaultValidationError extends SparkVaultMobileError {
|
|
29
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
30
|
+
super(message, { code: 'validation_error', statusCode: 400, details });
|
|
31
|
+
this.name = 'SparkVaultValidationError';
|
|
32
|
+
Object.setPrototypeOf(this, SparkVaultValidationError.prototype);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class SparkVaultAuthenticationError extends SparkVaultMobileError {
|
|
37
|
+
constructor(message = 'Authentication required', details?: Record<string, unknown>) {
|
|
38
|
+
super(message, { code: 'authentication_error', statusCode: 401, details });
|
|
39
|
+
this.name = 'SparkVaultAuthenticationError';
|
|
40
|
+
Object.setPrototypeOf(this, SparkVaultAuthenticationError.prototype);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SparkVaultAuthorizationError extends SparkVaultMobileError {
|
|
45
|
+
constructor(message = 'Forbidden', details?: Record<string, unknown>) {
|
|
46
|
+
super(message, { code: 'authorization_error', statusCode: 403, details });
|
|
47
|
+
this.name = 'SparkVaultAuthorizationError';
|
|
48
|
+
Object.setPrototypeOf(this, SparkVaultAuthorizationError.prototype);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class SparkVaultNetworkError extends SparkVaultMobileError {
|
|
53
|
+
constructor(message = 'Network request failed', details?: Record<string, unknown>) {
|
|
54
|
+
super(message, { code: 'network_error', details });
|
|
55
|
+
this.name = 'SparkVaultNetworkError';
|
|
56
|
+
Object.setPrototypeOf(this, SparkVaultNetworkError.prototype);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class SparkVaultTimeoutError extends SparkVaultMobileError {
|
|
61
|
+
constructor(message = 'Request timed out') {
|
|
62
|
+
super(message, { code: 'timeout_error', statusCode: 408 });
|
|
63
|
+
this.name = 'SparkVaultTimeoutError';
|
|
64
|
+
Object.setPrototypeOf(this, SparkVaultTimeoutError.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class TusUploadError extends SparkVaultMobileError {
|
|
69
|
+
readonly cause: Error | null;
|
|
70
|
+
readonly filename: string | null;
|
|
71
|
+
readonly fileSize: number | null;
|
|
72
|
+
readonly phase: 'create' | 'upload' | 'network' | 'timeout' | 'cancelled' | 'stalled' | 'unknown';
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
message: string,
|
|
76
|
+
options: {
|
|
77
|
+
cause?: Error | null;
|
|
78
|
+
httpStatus?: number | null;
|
|
79
|
+
filename?: string | null;
|
|
80
|
+
fileSize?: number | null;
|
|
81
|
+
phase?: TusUploadError['phase'];
|
|
82
|
+
} = {}
|
|
83
|
+
) {
|
|
84
|
+
super(message, {
|
|
85
|
+
code: options.httpStatus ?? 'tus_upload_error',
|
|
86
|
+
statusCode: options.httpStatus ?? undefined,
|
|
87
|
+
});
|
|
88
|
+
this.name = 'TusUploadError';
|
|
89
|
+
this.cause = options.cause ?? null;
|
|
90
|
+
this.filename = options.filename ?? null;
|
|
91
|
+
this.fileSize = options.fileSize ?? null;
|
|
92
|
+
this.phase = options.phase ?? 'unknown';
|
|
93
|
+
Object.setPrototypeOf(this, TusUploadError.prototype);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static fromError(
|
|
97
|
+
error: Error,
|
|
98
|
+
context: { filename?: string; fileSize?: number; phase?: TusUploadError['phase'] }
|
|
99
|
+
): TusUploadError {
|
|
100
|
+
const statusMatch = error.message.match(/(\d{3})/);
|
|
101
|
+
const httpStatus = statusMatch ? parseInt(statusMatch[1], 10) : null;
|
|
102
|
+
return new TusUploadError(error.message, {
|
|
103
|
+
cause: error,
|
|
104
|
+
httpStatus,
|
|
105
|
+
filename: context.filename,
|
|
106
|
+
fileSize: context.fileSize,
|
|
107
|
+
phase: context.phase ?? 'unknown',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/folders.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { MobileHttpClient } from './http.js';
|
|
2
|
+
import type {
|
|
3
|
+
CreateFolderRequest,
|
|
4
|
+
Folder,
|
|
5
|
+
FolderBreadcrumbItem,
|
|
6
|
+
ListFoldersResponse,
|
|
7
|
+
UpdateFolderRequest,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
import { validateFolderId, validateIngotId, validateVaultId } from './validation.js';
|
|
10
|
+
|
|
11
|
+
export class MobileFoldersClient {
|
|
12
|
+
private readonly http: MobileHttpClient;
|
|
13
|
+
|
|
14
|
+
constructor(http: MobileHttpClient) {
|
|
15
|
+
this.http = http;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async list(vaultId: string, vat: string): Promise<ListFoldersResponse> {
|
|
19
|
+
validateVaultId(vaultId);
|
|
20
|
+
const response = await this.http.get<ListFoldersResponse>(`/vaults/${vaultId}/folders`, { vat });
|
|
21
|
+
return response.data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async get(vaultId: string, folderId: string, vat: string): Promise<Folder> {
|
|
25
|
+
validateVaultId(vaultId);
|
|
26
|
+
validateFolderId(folderId);
|
|
27
|
+
const response = await this.http.get<Folder>(`/vaults/${vaultId}/folders/${folderId}`, { vat });
|
|
28
|
+
return response.data;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getBreadcrumb(vaultId: string, folderId: string, vat: string): Promise<FolderBreadcrumbItem[]> {
|
|
32
|
+
validateVaultId(vaultId);
|
|
33
|
+
validateFolderId(folderId);
|
|
34
|
+
const response = await this.http.get<{ breadcrumb: FolderBreadcrumbItem[] }>(
|
|
35
|
+
`/vaults/${vaultId}/folders/${folderId}/breadcrumb`,
|
|
36
|
+
{ vat }
|
|
37
|
+
);
|
|
38
|
+
return response.data.breadcrumb;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async create(vaultId: string, vat: string, data: CreateFolderRequest): Promise<Folder> {
|
|
42
|
+
validateVaultId(vaultId);
|
|
43
|
+
const response = await this.http.post<Folder>(`/vaults/${vaultId}/folders`, data, { vat });
|
|
44
|
+
return response.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async update(vaultId: string, folderId: string, vat: string, data: UpdateFolderRequest): Promise<Folder> {
|
|
48
|
+
validateVaultId(vaultId);
|
|
49
|
+
validateFolderId(folderId);
|
|
50
|
+
const response = await this.http.patch<Folder>(`/vaults/${vaultId}/folders/${folderId}`, data, { vat });
|
|
51
|
+
return response.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async move(vaultId: string, folderId: string, vat: string, newParentId: string | null): Promise<Folder> {
|
|
55
|
+
return this.update(vaultId, folderId, vat, { parent_id: newParentId });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async togglePin(vaultId: string, folderId: string, vat: string, pinned: boolean): Promise<Folder> {
|
|
59
|
+
return this.update(vaultId, folderId, vat, { pinned });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async delete(vaultId: string, folderId: string, vat: string): Promise<void> {
|
|
63
|
+
validateVaultId(vaultId);
|
|
64
|
+
validateFolderId(folderId);
|
|
65
|
+
await this.http.delete(`/vaults/${vaultId}/folders/${folderId}`, { vat });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async moveIngot(vaultId: string, ingotId: string, vat: string, folderId: string | null): Promise<void> {
|
|
69
|
+
validateVaultId(vaultId);
|
|
70
|
+
validateIngotId(ingotId);
|
|
71
|
+
if (folderId !== null) {
|
|
72
|
+
validateFolderId(folderId);
|
|
73
|
+
}
|
|
74
|
+
await this.http.patch(`/vaults/${vaultId}/ingots/${ingotId}`, { folder_id: folderId }, { vat });
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ResolvedMobileConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
export interface HealthCheckOptions {
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface HealthCheckResult {
|
|
8
|
+
online: boolean;
|
|
9
|
+
status: string;
|
|
10
|
+
httpStatus?: number;
|
|
11
|
+
checkedAt: number;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class MobileHealthClient {
|
|
16
|
+
private readonly config: ResolvedMobileConfig;
|
|
17
|
+
|
|
18
|
+
constructor(config: ResolvedMobileConfig) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async check(options: HealthCheckOptions = {}): Promise<HealthCheckResult> {
|
|
23
|
+
const checkedAt = Math.floor(Date.now() / 1000);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await this.config.fetch(this.healthUrl(), {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
headers: {
|
|
29
|
+
Accept: 'application/json',
|
|
30
|
+
'User-Agent': this.config.userAgent,
|
|
31
|
+
},
|
|
32
|
+
timeoutMs: options.timeoutMs ?? this.config.timeoutMs,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
online: response.ok,
|
|
37
|
+
status: response.ok ? await this.readStatus(response) : 'unhealthy',
|
|
38
|
+
httpStatus: response.status,
|
|
39
|
+
checkedAt,
|
|
40
|
+
};
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
this.config.logger.debug('Health check failed', { error: message });
|
|
44
|
+
return {
|
|
45
|
+
online: false,
|
|
46
|
+
status: 'unreachable',
|
|
47
|
+
checkedAt,
|
|
48
|
+
error: message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async isOnline(options: HealthCheckOptions = {}): Promise<boolean> {
|
|
54
|
+
const result = await this.check(options);
|
|
55
|
+
return result.online;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private healthUrl(): string {
|
|
59
|
+
return `${this.config.apiBaseUrl.replace(/\/v1$/, '')}/health`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async readStatus(response: Response): Promise<string> {
|
|
63
|
+
try {
|
|
64
|
+
const body = await response.json();
|
|
65
|
+
const data = typeof body === 'object' && body !== null && 'data' in body
|
|
66
|
+
? (body as { data?: unknown }).data
|
|
67
|
+
: body;
|
|
68
|
+
|
|
69
|
+
if (typeof data === 'object' && data !== null) {
|
|
70
|
+
const status = (data as { status?: unknown }).status;
|
|
71
|
+
if (typeof status === 'string' && status) {
|
|
72
|
+
return status;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
80
|
+
}
|
|
81
|
+
}
|