@sparkvault/sdk-mobile 0.3.3 → 1.0.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/dist/auth.d.ts +21 -1
- package/dist/auth.js +82 -31
- package/dist/auth.js.map +1 -1
- package/dist/config.js +8 -4
- package/dist/config.js.map +1 -1
- package/dist/encoding.js +15 -5
- package/dist/encoding.js.map +1 -1
- package/dist/http.js +15 -2
- package/dist/http.js.map +1 -1
- package/dist/identity-dialog.js +3 -4
- package/dist/identity-dialog.js.map +1 -1
- package/dist/ingots.js +2 -2
- package/dist/ingots.js.map +1 -1
- package/dist/platform-domains.generated.d.ts +2 -0
- package/dist/platform-domains.generated.js +16 -0
- package/dist/platform-domains.generated.js.map +1 -0
- package/dist/tus.d.ts +8 -0
- package/dist/tus.js +38 -1
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +20 -1
- package/dist/validation.d.ts +6 -0
- package/dist/validation.js +13 -6
- package/dist/validation.js.map +1 -1
- package/dist/vaults.d.ts +0 -8
- package/dist/vaults.js +0 -12
- package/dist/vaults.js.map +1 -1
- package/package.json +3 -2
- package/src/auth.ts +96 -32
- package/src/config.ts +12 -4
- package/src/encoding.ts +15 -5
- package/src/http.ts +13 -2
- package/src/identity-dialog.tsx +3 -4
- package/src/ingots.ts +2 -4
- package/src/platform-domains.generated.ts +17 -0
- package/src/tus.ts +42 -1
- package/src/types.ts +25 -1
- package/src/validation.ts +14 -7
- package/src/vaults.ts +0 -14
package/src/auth.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
SendTotpRequest,
|
|
26
26
|
SparkVaultAccount,
|
|
27
27
|
SparkVaultUser,
|
|
28
|
+
SubmitSecondFactorRequest,
|
|
28
29
|
VerifyTotpRequest,
|
|
29
30
|
} from './types.js';
|
|
30
31
|
import nacl from 'tweetnacl';
|
|
@@ -39,6 +40,12 @@ interface IdentityVerificationResponse {
|
|
|
39
40
|
kindling?: string;
|
|
40
41
|
expires_at?: number;
|
|
41
42
|
retry_after?: number;
|
|
43
|
+
/** Primary factor passed, second factor still owed. */
|
|
44
|
+
second_factor_required?: boolean;
|
|
45
|
+
/** Pending-login handle for the second-factor submit. */
|
|
46
|
+
ticket?: string;
|
|
47
|
+
/** Offered second-factor methods. */
|
|
48
|
+
methods?: string[];
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
interface IdentityPasskeyCompletionResponse {
|
|
@@ -171,9 +178,6 @@ export class MobileAuthClient {
|
|
|
171
178
|
if (claims.nbf !== undefined && claims.nbf - clockSkewSeconds > now) {
|
|
172
179
|
throw new SparkVaultValidationError('Identity token is not valid yet');
|
|
173
180
|
}
|
|
174
|
-
if (typeof claims.nbf === 'number' && claims.nbf > now + clockSkewSeconds) {
|
|
175
|
-
throw new SparkVaultValidationError('Identity token is not yet valid');
|
|
176
|
-
}
|
|
177
181
|
|
|
178
182
|
const expectedIssuer = `${this.config.identityBaseUrl}/${this.config.identityAccountId}`;
|
|
179
183
|
if (claims.iss !== expectedIssuer) {
|
|
@@ -249,23 +253,15 @@ export class MobileAuthClient {
|
|
|
249
253
|
|
|
250
254
|
async verifyTotp(request: VerifyTotpRequest): Promise<IdentityVerifyResult> {
|
|
251
255
|
const verifyResult = await this.verifyTotpWithRetryErrors(request);
|
|
252
|
-
if (!verifyResult.verified || !verifyResult.token) {
|
|
253
|
-
if (verifyResult.kindling) {
|
|
254
|
-
const error = new SparkVaultMobileError('Invalid code. Please try again.', {
|
|
255
|
-
code: 400,
|
|
256
|
-
statusCode: 400,
|
|
257
|
-
});
|
|
258
|
-
(error as SparkVaultMobileError & { data?: unknown }).data = {
|
|
259
|
-
kindling: verifyResult.kindling,
|
|
260
|
-
expires_at: verifyResult.expires_at,
|
|
261
|
-
};
|
|
262
|
-
throw error;
|
|
263
|
-
}
|
|
264
256
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
257
|
+
// PIN accepted, but the account requires a second factor. Surface the
|
|
258
|
+
// ticket so the caller can collect a code and call submitSecondFactor().
|
|
259
|
+
if (verifyResult.second_factor_required && verifyResult.ticket) {
|
|
260
|
+
throw this.secondFactorError(verifyResult.ticket, verifyResult.methods);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!verifyResult.verified || !verifyResult.token) {
|
|
264
|
+
this.throwVerificationFailure(verifyResult);
|
|
269
265
|
}
|
|
270
266
|
|
|
271
267
|
return this.toIdentityVerifyResult(verifyResult.token, {
|
|
@@ -279,26 +275,94 @@ export class MobileAuthClient {
|
|
|
279
275
|
async verifyPin(request: PinVerifyRequest): Promise<PinVerifyResponse> {
|
|
280
276
|
const verifyResult = await this.verifyTotpWithRetryErrors(request);
|
|
281
277
|
|
|
278
|
+
// PIN accepted, but 2FA is required. Return a pending result so the caller
|
|
279
|
+
// can collect a second factor and call completeSecondFactorPin().
|
|
280
|
+
if (verifyResult.second_factor_required && verifyResult.ticket) {
|
|
281
|
+
return {
|
|
282
|
+
verified: false,
|
|
283
|
+
second_factor_required: true,
|
|
284
|
+
ticket: verifyResult.ticket,
|
|
285
|
+
methods: verifyResult.methods ?? ['authenticator'],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
282
289
|
if (!verifyResult.verified || !verifyResult.token) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
this.throwVerificationFailure(verifyResult);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return this.exchangeIdentityToken(verifyResult.token);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Surface a failed primary-factor verification. When the backend returns a
|
|
298
|
+
* retry kindling, it is grafted onto the error's `data` so the caller can
|
|
299
|
+
* offer another attempt against the same code session.
|
|
300
|
+
*/
|
|
301
|
+
private throwVerificationFailure(verifyResult: IdentityVerificationResponse): never {
|
|
302
|
+
if (verifyResult.kindling) {
|
|
303
|
+
const error = new SparkVaultMobileError('Invalid code. Please try again.', {
|
|
304
|
+
code: 400,
|
|
305
|
+
statusCode: 400,
|
|
306
|
+
});
|
|
307
|
+
(error as SparkVaultMobileError & { data?: unknown }).data = {
|
|
308
|
+
kindling: verifyResult.kindling,
|
|
309
|
+
expires_at: verifyResult.expires_at,
|
|
310
|
+
};
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
294
313
|
|
|
295
|
-
|
|
314
|
+
throw new SparkVaultMobileError('Verification failed. Please request a new code.', {
|
|
315
|
+
code: 400,
|
|
316
|
+
statusCode: 400,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Complete a login held at the second-factor gate (from verifyTotp). Submits
|
|
322
|
+
* a 6-digit authenticator code or a recovery code and returns the verified
|
|
323
|
+
* identity result. A wrong code throws.
|
|
324
|
+
*/
|
|
325
|
+
async submitSecondFactor(request: SubmitSecondFactorRequest): Promise<IdentityVerifyResult> {
|
|
326
|
+
const result = await this.postIdentity<IdentityVerificationResponse>('/second-factor/verify', {
|
|
327
|
+
ticket: request.ticket,
|
|
328
|
+
code: request.code,
|
|
329
|
+
});
|
|
330
|
+
if (!result.verified || !result.token) {
|
|
331
|
+
throw new SparkVaultMobileError('That code is not valid. Please try again.', {
|
|
296
332
|
code: 400,
|
|
297
333
|
statusCode: 400,
|
|
298
334
|
});
|
|
299
335
|
}
|
|
336
|
+
return this.toIdentityVerifyResult(result.token, {
|
|
337
|
+
identity: result.identity,
|
|
338
|
+
identityType: result.identity_type,
|
|
339
|
+
method: result.method,
|
|
340
|
+
redirect: result.redirect,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
300
343
|
|
|
301
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Complete a 2FA-pending app login (from verifyPin's pending response):
|
|
346
|
+
* submit the second factor, then exchange the identity token for an app
|
|
347
|
+
* session.
|
|
348
|
+
*/
|
|
349
|
+
async completeSecondFactorPin(request: SubmitSecondFactorRequest): Promise<PinVerifyResponse> {
|
|
350
|
+
const result = await this.submitSecondFactor(request);
|
|
351
|
+
return this.exchangeIdentityToken(result.token);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Build the typed error verifyTotp throws when a second factor is required. */
|
|
355
|
+
private secondFactorError(ticket: string, methods?: string[]): SparkVaultMobileError {
|
|
356
|
+
const error = new SparkVaultMobileError('Two-step verification required.', {
|
|
357
|
+
code: 401,
|
|
358
|
+
statusCode: 401,
|
|
359
|
+
});
|
|
360
|
+
(error as SparkVaultMobileError & { data?: unknown }).data = {
|
|
361
|
+
second_factor_required: true,
|
|
362
|
+
ticket,
|
|
363
|
+
methods: methods ?? ['authenticator'],
|
|
364
|
+
};
|
|
365
|
+
return error;
|
|
302
366
|
}
|
|
303
367
|
|
|
304
368
|
async completeSignup(
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { SparkVaultValidationError } from './errors.js';
|
|
2
|
+
import {
|
|
3
|
+
SPARKVAULT_PLATFORM_SV_HOSTS,
|
|
4
|
+
SPARKVAULT_ROOT_DOMAIN,
|
|
5
|
+
} from './platform-domains.generated.js';
|
|
2
6
|
import type {
|
|
3
7
|
FetchLike,
|
|
4
8
|
MobileFileDownloader,
|
|
@@ -10,11 +14,15 @@ import type {
|
|
|
10
14
|
const DEFAULT_API_BASE_URL = 'https://api.sparkvault.com/v1';
|
|
11
15
|
const DEFAULT_IDENTITY_BASE_URL = 'https://api.sparkvault.com/v1/products/identity';
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
function escapeRegExp(value: string): string {
|
|
18
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Hosts that are allowed to serve backend-issued ingot download URLs, derived
|
|
22
|
+
// from the canonical platform domain registry baked in at build time.
|
|
15
23
|
const DEFAULT_ALLOWED_DOWNLOAD_HOST_PATTERNS: RegExp[] = [
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
new RegExp(`(^|\\.)${escapeRegExp(SPARKVAULT_ROOT_DOMAIN)}$`, 'i'),
|
|
25
|
+
new RegExp(`(^|\\.)(${SPARKVAULT_PLATFORM_SV_HOSTS.map(escapeRegExp).join('|')})$`, 'i'),
|
|
18
26
|
/\.amazonaws\.com$/i,
|
|
19
27
|
/\.cloudfront\.net$/i,
|
|
20
28
|
];
|
package/src/encoding.ts
CHANGED
|
@@ -123,7 +123,17 @@ export function base64ToBytes(value: string): Uint8Array {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const padded = sanitized.padEnd(Math.ceil(sanitized.length / 4) * 4, '=');
|
|
126
|
-
|
|
126
|
+
|
|
127
|
+
// Preallocate the exact output size: 3 bytes per quartet, minus one byte per
|
|
128
|
+
// '=' (a '=' in quartet positions 1-2 throws below, so counting every '=' is
|
|
129
|
+
// safe). Multi-megabyte upload chunks decode through here, so per-byte array
|
|
130
|
+
// growth is not acceptable.
|
|
131
|
+
let paddingCount = 0;
|
|
132
|
+
for (let i = 0; i < padded.length; i++) {
|
|
133
|
+
if (padded[i] === '=') paddingCount++;
|
|
134
|
+
}
|
|
135
|
+
const bytes = new Uint8Array((padded.length / 4) * 3 - paddingCount);
|
|
136
|
+
let outIndex = 0;
|
|
127
137
|
|
|
128
138
|
for (let i = 0; i < padded.length; i += 4) {
|
|
129
139
|
const c1 = padded[i];
|
|
@@ -141,10 +151,10 @@ export function base64ToBytes(value: string): Uint8Array {
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
const triplet = (v1 << 18) | (v2 << 12) | (v3 << 6) | v4;
|
|
144
|
-
bytes
|
|
145
|
-
if (c3 !== '=') bytes
|
|
146
|
-
if (c4 !== '=') bytes
|
|
154
|
+
bytes[outIndex++] = (triplet >> 16) & 0xff;
|
|
155
|
+
if (c3 !== '=') bytes[outIndex++] = (triplet >> 8) & 0xff;
|
|
156
|
+
if (c4 !== '=') bytes[outIndex++] = triplet & 0xff;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
return
|
|
159
|
+
return bytes;
|
|
150
160
|
}
|
package/src/http.ts
CHANGED
|
@@ -231,9 +231,20 @@ export class MobileHttpClient {
|
|
|
231
231
|
return false;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
// Refresh tokens rotate and are one-time-use: persist the pair through a
|
|
235
|
+
// single adapter call when the adapter supports it, so the write can be
|
|
236
|
+
// atomic. Two separate writes risk a process kill stranding a new access
|
|
237
|
+
// token alongside the already-consumed refresh token (forced logout on
|
|
238
|
+
// the next refresh).
|
|
235
239
|
if (typeof data.refresh_token === 'string' && data.refresh_token) {
|
|
236
|
-
|
|
240
|
+
if (this.config.tokenStorage.setTokens) {
|
|
241
|
+
await this.config.tokenStorage.setTokens(data.access_token, data.refresh_token);
|
|
242
|
+
} else {
|
|
243
|
+
await this.config.tokenStorage.setAccessToken(data.access_token);
|
|
244
|
+
await this.config.tokenStorage.setRefreshToken(data.refresh_token);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
await this.config.tokenStorage.setAccessToken(data.access_token);
|
|
237
248
|
}
|
|
238
249
|
this.emitAuthEvent('token_refreshed');
|
|
239
250
|
return true;
|
package/src/identity-dialog.tsx
CHANGED
|
@@ -402,7 +402,7 @@ export function SparkVaultIdentityDialog({
|
|
|
402
402
|
hitSlop={12}
|
|
403
403
|
style={styles.closeButton}
|
|
404
404
|
>
|
|
405
|
-
<Text style={[styles.closeText, { color: colors.muted }]}
|
|
405
|
+
<Text style={[styles.closeText, { color: colors.muted }]}>✕</Text>
|
|
406
406
|
</Pressable>
|
|
407
407
|
</View>
|
|
408
408
|
|
|
@@ -496,7 +496,7 @@ export function SparkVaultIdentityDialog({
|
|
|
496
496
|
Confirm this sign-in with your device passkey.
|
|
497
497
|
</Text>
|
|
498
498
|
<View style={[styles.passkeyIcon, { backgroundColor: colors.primarySoft }]}>
|
|
499
|
-
<Text style={
|
|
499
|
+
<Text style={styles.passkeyIconText}>🔑</Text>
|
|
500
500
|
</View>
|
|
501
501
|
{isBusy ? <ActivityIndicator color={colors.primary} /> : null}
|
|
502
502
|
{error ? <Text style={[styles.error, { color: colors.error }]}>{error}</Text> : null}
|
|
@@ -974,7 +974,6 @@ const styles = StyleSheet.create({
|
|
|
974
974
|
justifyContent: 'center',
|
|
975
975
|
},
|
|
976
976
|
passkeyIconText: {
|
|
977
|
-
fontSize:
|
|
978
|
-
fontWeight: '800',
|
|
977
|
+
fontSize: 34,
|
|
979
978
|
},
|
|
980
979
|
});
|
package/src/ingots.ts
CHANGED
|
@@ -89,8 +89,6 @@ interface IngotAuditLogEntry {
|
|
|
89
89
|
|
|
90
90
|
interface IngotAuditLogsResponse {
|
|
91
91
|
entries?: IngotAuditLogEntry[];
|
|
92
|
-
logs?: IngotAuditLogEntry[];
|
|
93
|
-
cursor?: string;
|
|
94
92
|
next_cursor?: string;
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -456,10 +454,10 @@ export class MobileIngotsClient {
|
|
|
456
454
|
: `/vaults/${vaultId}/ingots/${ingotId}/audit-logs`,
|
|
457
455
|
{ vat }
|
|
458
456
|
);
|
|
459
|
-
const entries = response.data.entries ??
|
|
457
|
+
const entries = response.data.entries ?? [];
|
|
460
458
|
return {
|
|
461
459
|
logs: entries.map(entry => this.toAccessLog(entry)),
|
|
462
|
-
next_cursor: response.data.
|
|
460
|
+
next_cursor: response.data.next_cursor,
|
|
463
461
|
};
|
|
464
462
|
}
|
|
465
463
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Source of truth: packages/app-sdk/src/platform-domains.js
|
|
3
|
+
// Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
|
|
4
|
+
|
|
5
|
+
export const SPARKVAULT_ROOT_DOMAIN = "sparkvault.com";
|
|
6
|
+
|
|
7
|
+
export const SPARKVAULT_PLATFORM_SV_HOSTS: readonly string[] = Object.freeze([
|
|
8
|
+
"auth.sv",
|
|
9
|
+
"x.sv",
|
|
10
|
+
"files.sv",
|
|
11
|
+
"file.sv",
|
|
12
|
+
"send.sv",
|
|
13
|
+
"spark.sv",
|
|
14
|
+
"by.sv",
|
|
15
|
+
"at.sv",
|
|
16
|
+
"db.sv",
|
|
17
|
+
]);
|
package/src/tus.ts
CHANGED
|
@@ -73,6 +73,12 @@ function ensureNotAborted(signal?: AbortSignal): void {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
76
|
+
// base64ToBytes returns exactly-sized buffers, so the chunk hot path is a
|
|
77
|
+
// zero-copy unwrap; copy only when the view spans part of a larger buffer.
|
|
78
|
+
// Aliasing is safe: chunks are sent then discarded, never mutated.
|
|
79
|
+
if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
80
|
+
return bytes.buffer as ArrayBuffer;
|
|
81
|
+
}
|
|
76
82
|
const copy = new Uint8Array(bytes.byteLength);
|
|
77
83
|
copy.set(bytes);
|
|
78
84
|
return copy.buffer;
|
|
@@ -223,10 +229,11 @@ export class MobileTusUploader {
|
|
|
223
229
|
const debug = options.debug;
|
|
224
230
|
const cleanUri = options.fileUri.split('#')[0];
|
|
225
231
|
let bytesUploaded = 0;
|
|
232
|
+
let uploadUrl: string | null = null;
|
|
226
233
|
|
|
227
234
|
try {
|
|
228
235
|
debug?.log(`Starting TUS upload for ${options.filename}`);
|
|
229
|
-
|
|
236
|
+
uploadUrl = await createTusUpload(
|
|
230
237
|
this.config,
|
|
231
238
|
parsed,
|
|
232
239
|
options.fileSize,
|
|
@@ -267,6 +274,18 @@ export class MobileTusUploader {
|
|
|
267
274
|
} catch (err) {
|
|
268
275
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
269
276
|
debug?.error(`Upload failed: ${getErrorMessage(error)}`);
|
|
277
|
+
|
|
278
|
+
// A cancelled session must be terminated server-side: the tus DELETE is
|
|
279
|
+
// what lets Forge remove already-written chunk objects (otherwise they
|
|
280
|
+
// are orphaned in S3 — nothing else cleans them up) and settle
|
|
281
|
+
// partial-transfer billing, matching the web client's abort semantics.
|
|
282
|
+
const cancelled =
|
|
283
|
+
options.abortSignal?.aborted === true ||
|
|
284
|
+
(err instanceof TusUploadError && err.phase === 'cancelled');
|
|
285
|
+
if (cancelled && uploadUrl) {
|
|
286
|
+
this.terminateUpload(uploadUrl, parsed.istk, debug);
|
|
287
|
+
}
|
|
288
|
+
|
|
270
289
|
if (err instanceof TusUploadError) throw err;
|
|
271
290
|
throw TusUploadError.fromError(error, {
|
|
272
291
|
filename: options.filename,
|
|
@@ -276,6 +295,28 @@ export class MobileTusUploader {
|
|
|
276
295
|
}
|
|
277
296
|
}
|
|
278
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Best-effort tus termination after a cancelled upload. Fire-and-forget so
|
|
300
|
+
* cancel UX stays instant and the cancellation error still propagates — a
|
|
301
|
+
* failed DELETE only means the orphaned session waits for server-side
|
|
302
|
+
* expiry. Deliberately NOT bound to the (already aborted) upload signal,
|
|
303
|
+
* which would kill the DELETE before it left the device.
|
|
304
|
+
*/
|
|
305
|
+
private terminateUpload(uploadUrl: string, istk: string, debug?: DebugLogger): void {
|
|
306
|
+
void this.config
|
|
307
|
+
.fetch(uploadUrl, {
|
|
308
|
+
method: 'DELETE',
|
|
309
|
+
headers: {
|
|
310
|
+
'Tus-Resumable': TUS_VERSION,
|
|
311
|
+
'X-ISTK': istk,
|
|
312
|
+
},
|
|
313
|
+
timeoutMs: this.config.tusPostTimeoutMs,
|
|
314
|
+
})
|
|
315
|
+
.catch(err => {
|
|
316
|
+
debug?.log(`TUS termination after cancel failed: ${getErrorMessage(err)}`);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
279
320
|
private async readChunk(
|
|
280
321
|
fileReader: MobileFileReader,
|
|
281
322
|
fileUri: string,
|
package/src/types.ts
CHANGED
|
@@ -174,7 +174,31 @@ export interface PinVerifySignupPendingResponse {
|
|
|
174
174
|
expires_at: number;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Returned by verifyPin when the PIN was correct but the account has 2FA
|
|
179
|
+
* enabled: the caller must collect a second factor and call
|
|
180
|
+
* completeSecondFactorPin({ ticket, code }) to finish the login.
|
|
181
|
+
*/
|
|
182
|
+
export interface PinVerifySecondFactorPendingResponse {
|
|
183
|
+
verified: false;
|
|
184
|
+
second_factor_required: true;
|
|
185
|
+
/** Opaque pending-login handle to submit the second factor against. */
|
|
186
|
+
ticket: string;
|
|
187
|
+
/** Methods the user may use, e.g. ['authenticator','recovery_code']. */
|
|
188
|
+
methods: string[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export type PinVerifyResponse =
|
|
192
|
+
| PinVerifyLoginResponse
|
|
193
|
+
| PinVerifySignupPendingResponse
|
|
194
|
+
| PinVerifySecondFactorPendingResponse;
|
|
195
|
+
|
|
196
|
+
export interface SubmitSecondFactorRequest {
|
|
197
|
+
/** Pending-login ticket from a second_factor_required response. */
|
|
198
|
+
ticket: string;
|
|
199
|
+
/** 6-digit authenticator code or a recovery code. */
|
|
200
|
+
code: string;
|
|
201
|
+
}
|
|
178
202
|
|
|
179
203
|
export interface CompleteSignupRequest {
|
|
180
204
|
organization_name: string;
|
package/src/validation.ts
CHANGED
|
@@ -25,13 +25,20 @@ export function validateFolderId(folderId: string): void {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Windows reserved filenames that cannot be used. */
|
|
29
|
+
const WINDOWS_RESERVED_NAMES = new Set([
|
|
30
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
|
31
|
+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
32
|
+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sanitize a filename to prevent path traversal and other security issues:
|
|
37
|
+
* removes null bytes and control characters, strips path traversal sequences,
|
|
38
|
+
* replaces directory separators, strips leading dots (hidden files), and
|
|
39
|
+
* prefixes Windows reserved names. Falls back to 'file' when nothing remains.
|
|
40
|
+
*/
|
|
28
41
|
export function sanitizeFilename(filename: string): string {
|
|
29
|
-
const reservedNames = new Set([
|
|
30
|
-
'CON', 'PRN', 'AUX', 'NUL',
|
|
31
|
-
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
|
32
|
-
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
42
|
let sanitized = filename
|
|
36
43
|
.replace(/\0/g, '')
|
|
37
44
|
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
@@ -41,7 +48,7 @@ export function sanitizeFilename(filename: string): string {
|
|
|
41
48
|
.trim();
|
|
42
49
|
|
|
43
50
|
const baseName = sanitized.split('.')[0].toUpperCase();
|
|
44
|
-
if (
|
|
51
|
+
if (WINDOWS_RESERVED_NAMES.has(baseName)) {
|
|
45
52
|
sanitized = `_${sanitized}`;
|
|
46
53
|
}
|
|
47
54
|
|
package/src/vaults.ts
CHANGED
|
@@ -257,20 +257,6 @@ export class MobileVaultsClient {
|
|
|
257
257
|
return response.data;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
/**
|
|
261
|
-
* @deprecated Use enableUploadPortal() or enableUploadWidget().
|
|
262
|
-
*/
|
|
263
|
-
async enableUpload(vaultId: string, vmk: string): Promise<UploadPortalEnabled> {
|
|
264
|
-
return this.enableUploadPortal(vaultId, vmk);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* @deprecated Use disableUploadPortal() or disableUploadWidget().
|
|
269
|
-
*/
|
|
270
|
-
async disableUpload(vaultId: string): Promise<UploadPortalDisabled> {
|
|
271
|
-
return this.disableUploadPortal(vaultId);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
260
|
async updateUploadConfig(
|
|
275
261
|
vaultId: string,
|
|
276
262
|
config: {
|