@sparkvault/sdk-mobile 6.0.2 → 7.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 +5 -5
- package/dist/account.d.ts +4 -3
- package/dist/account.js +16 -11
- package/dist/account.js.map +1 -1
- package/dist/app-sdk/email-address.d.ts +37 -0
- package/dist/app-sdk/email-address.js +91 -0
- package/dist/app-sdk/email-address.js.map +1 -0
- package/dist/app-sdk/encoding.d.ts +64 -0
- package/dist/app-sdk/encoding.js +245 -0
- package/dist/app-sdk/encoding.js.map +1 -0
- package/dist/app-sdk/entropy-contract.d.ts +33 -0
- package/dist/app-sdk/entropy-contract.js +51 -0
- package/dist/app-sdk/entropy-contract.js.map +1 -0
- package/dist/app-sdk/filename.d.ts +93 -0
- package/dist/app-sdk/filename.js +161 -0
- package/dist/app-sdk/filename.js.map +1 -0
- package/dist/app-sdk/forge-url.d.ts +107 -0
- package/dist/app-sdk/forge-url.js +222 -0
- package/dist/app-sdk/forge-url.js.map +1 -0
- package/dist/app-sdk/ids.d.ts +48 -0
- package/dist/app-sdk/ids.js +65 -0
- package/dist/app-sdk/ids.js.map +1 -0
- package/dist/app-sdk/ingot-access-log.d.ts +29 -0
- package/dist/app-sdk/ingot-access-log.js +49 -0
- package/dist/app-sdk/ingot-access-log.js.map +1 -0
- package/dist/app-sdk/phone.d.ts +37 -0
- package/dist/app-sdk/phone.js +134 -0
- package/dist/app-sdk/phone.js.map +1 -0
- package/dist/app-sdk/platform-domains.d.ts +41 -0
- package/dist/app-sdk/platform-domains.js +82 -0
- package/dist/app-sdk/platform-domains.js.map +1 -0
- package/dist/app-sdk/request-path.d.ts +49 -0
- package/dist/app-sdk/request-path.js +81 -0
- package/dist/app-sdk/request-path.js.map +1 -0
- package/dist/app-sdk/transient.d.ts +23 -0
- package/dist/app-sdk/transient.js +29 -0
- package/dist/app-sdk/transient.js.map +1 -0
- package/dist/app-sdk/vat-refusal.d.ts +66 -0
- package/dist/app-sdk/vat-refusal.js +69 -0
- package/dist/app-sdk/vat-refusal.js.map +1 -0
- package/dist/billing.d.ts +9 -17
- package/dist/billing.js +14 -17
- package/dist/billing.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.js +10 -17
- package/dist/config.js.map +1 -1
- package/dist/encoding.d.ts +30 -4
- package/dist/encoding.js +89 -83
- package/dist/encoding.js.map +1 -1
- package/dist/entropy.d.ts +25 -4
- package/dist/entropy.js +62 -15
- package/dist/entropy.js.map +1 -1
- package/dist/entropy.test-d.d.ts +40 -0
- package/dist/entropy.test-d.js +12 -0
- package/dist/entropy.test-d.js.map +1 -0
- package/dist/errors.d.ts +67 -17
- package/dist/errors.js +70 -27
- package/dist/errors.js.map +1 -1
- package/dist/folders.js +8 -7
- package/dist/folders.js.map +1 -1
- package/dist/http.d.ts +57 -15
- package/dist/http.js +125 -138
- package/dist/http.js.map +1 -1
- package/dist/identity-dialog.js +4 -30
- package/dist/identity-dialog.js.map +1 -1
- package/dist/identity-input.d.ts +39 -0
- package/dist/identity-input.js +59 -0
- package/dist/identity-input.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +51 -18
- package/dist/ingots.js +132 -122
- package/dist/ingots.js.map +1 -1
- package/dist/mutex.d.ts +1 -2
- package/dist/mutex.js +0 -6
- package/dist/mutex.js.map +1 -1
- package/dist/notify.d.ts +7 -2
- package/dist/notify.js +12 -15
- package/dist/notify.js.map +1 -1
- package/dist/notify.test-d.d.ts +3 -2
- package/dist/notify.test-d.js +1 -1
- package/dist/products/identity.d.ts +5 -0
- package/dist/products/identity.js +40 -35
- package/dist/products/identity.js.map +1 -1
- package/dist/request-path.d.ts +32 -0
- package/dist/request-path.js +35 -0
- package/dist/request-path.js.map +1 -0
- package/dist/sparks.js +16 -15
- package/dist/sparks.js.map +1 -1
- package/dist/tus.d.ts +19 -2
- package/dist/tus.js +86 -35
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +167 -15
- package/dist/users.d.ts +12 -5
- package/dist/users.js +27 -19
- package/dist/users.js.map +1 -1
- package/dist/validation.d.ts +20 -5
- package/dist/validation.js +29 -27
- package/dist/validation.js.map +1 -1
- package/dist/vaults.d.ts +34 -83
- package/dist/vaults.js +64 -44
- package/dist/vaults.js.map +1 -1
- package/dist/vaults.test-d.d.ts +22 -0
- package/dist/vaults.test-d.js +14 -0
- package/dist/vaults.test-d.js.map +1 -0
- package/package.json +4 -2
- package/src/account.ts +16 -11
- package/src/app-sdk/email-address.js +94 -0
- package/src/app-sdk/encoding.js +250 -0
- package/src/app-sdk/entropy-contract.js +56 -0
- package/src/app-sdk/filename.js +174 -0
- package/src/app-sdk/forge-url.js +224 -0
- package/src/app-sdk/ids.js +71 -0
- package/src/app-sdk/ingot-access-log.js +51 -0
- package/src/app-sdk/phone.js +139 -0
- package/src/app-sdk/platform-domains.js +99 -0
- package/src/app-sdk/request-path.js +81 -0
- package/src/app-sdk/transient.js +30 -0
- package/src/app-sdk/vat-refusal.js +74 -0
- package/src/billing.ts +16 -25
- package/src/config.ts +16 -23
- package/src/encoding.ts +91 -103
- package/src/entropy.test-d.ts +47 -0
- package/src/entropy.ts +86 -20
- package/src/errors.ts +93 -33
- package/src/folders.ts +8 -7
- package/src/http.ts +145 -150
- package/src/identity-dialog.tsx +4 -37
- package/src/identity-input.ts +70 -0
- package/src/index.ts +11 -3
- package/src/ingots.ts +173 -159
- package/src/mutex.ts +1 -8
- package/src/notify.test-d.ts +3 -1
- package/src/notify.ts +19 -17
- package/src/products/identity.ts +45 -39
- package/src/request-path.ts +45 -0
- package/src/sparks.ts +16 -13
- package/src/tus.ts +90 -37
- package/src/types.ts +165 -15
- package/src/users.ts +27 -17
- package/src/validation.ts +30 -24
- package/src/vaults.test-d.ts +29 -0
- package/src/vaults.ts +87 -153
- package/dist/platform-domains.generated.d.ts +0 -2
- package/dist/platform-domains.generated.js +0 -16
- package/dist/platform-domains.generated.js.map +0 -1
- package/src/platform-domains.generated.ts +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkvault/sdk-mobile",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Mobile SDK for SparkVault Identity, vault, folder, and ingot workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"scripts": {
|
|
30
|
-
"build": "node scripts/generate-
|
|
30
|
+
"build": "node scripts/generate-app-sdk-mirrors.mjs && tsc",
|
|
31
31
|
"prepack": "pnpm build",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"test": "vitest run"
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
"directory": "packages/sdk-mobile"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
+
"@babel/core": "^7.29.7",
|
|
59
|
+
"@babel/plugin-transform-flow-strip-types": "^7.27.1",
|
|
58
60
|
"@sparkvault/app-sdk": "workspace:*",
|
|
59
61
|
"@types/react": "~19.1.0",
|
|
60
62
|
"react": "19.1.0",
|
package/src/account.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
SparkVaultUser,
|
|
11
11
|
UpdateAccountRequest,
|
|
12
12
|
} from './types.js';
|
|
13
|
+
import { apiPath } from './request-path.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* First-party SparkVault Core session, profile, and account client. Exchanges a
|
|
@@ -27,7 +28,7 @@ export class MobileAccountClient {
|
|
|
27
28
|
|
|
28
29
|
async exchangeIdentityToken(identityToken: string): Promise<PinVerifyResponse> {
|
|
29
30
|
const sessionResponse = await this.http.post<IdentitySessionResponse>(
|
|
30
|
-
|
|
31
|
+
apiPath`/auth/identity/verify`,
|
|
31
32
|
{ token: identityToken },
|
|
32
33
|
{ skipAuth: true }
|
|
33
34
|
);
|
|
@@ -57,7 +58,7 @@ export class MobileAccountClient {
|
|
|
57
58
|
request: CompleteSignupRequest
|
|
58
59
|
): Promise<CompleteSignupResponse> {
|
|
59
60
|
const response = await this.http.post<CompleteSignupResponse>(
|
|
60
|
-
|
|
61
|
+
apiPath`/auth/complete-signup`,
|
|
61
62
|
request,
|
|
62
63
|
{
|
|
63
64
|
skipAuth: true,
|
|
@@ -81,7 +82,7 @@ export class MobileAccountClient {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
async getProfile(): Promise<SparkVaultUser> {
|
|
84
|
-
const response = await this.http.get<SparkVaultUser>(
|
|
85
|
+
const response = await this.http.get<SparkVaultUser>(apiPath`/profile`);
|
|
85
86
|
if (this.config.tokenStorage.setUser) {
|
|
86
87
|
await this.config.tokenStorage.setUser(response.data);
|
|
87
88
|
}
|
|
@@ -89,7 +90,7 @@ export class MobileAccountClient {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async updateProfile(updates: { name?: string }): Promise<SparkVaultUser> {
|
|
92
|
-
const response = await this.http.put<SparkVaultUser>(
|
|
93
|
+
const response = await this.http.put<SparkVaultUser>(apiPath`/profile`, updates);
|
|
93
94
|
if (this.config.tokenStorage.setUser) {
|
|
94
95
|
await this.config.tokenStorage.setUser(response.data);
|
|
95
96
|
}
|
|
@@ -97,7 +98,7 @@ export class MobileAccountClient {
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
async getAccount(): Promise<SparkVaultAccount> {
|
|
100
|
-
const response = await this.http.get<SparkVaultAccount>(
|
|
101
|
+
const response = await this.http.get<SparkVaultAccount>(apiPath`/account`);
|
|
101
102
|
if (this.config.tokenStorage.setAccount) {
|
|
102
103
|
await this.config.tokenStorage.setAccount(response.data);
|
|
103
104
|
}
|
|
@@ -106,20 +107,24 @@ export class MobileAccountClient {
|
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* Update organization settings (member admins only). Sends a JSON body — the
|
|
109
|
-
* logo-upload variant of PUT /account is web-only. The PUT
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
* logo-upload variant of PUT /account is web-only. The PUT answers with the
|
|
111
|
+
* account as it now stands (the same projection GET /account serves), which
|
|
112
|
+
* refreshes the cache as is: a read right after the write could land on a
|
|
113
|
+
* replica that has not seen it yet.
|
|
112
114
|
*/
|
|
113
115
|
async updateAccount(updates: UpdateAccountRequest): Promise<SparkVaultAccount> {
|
|
114
|
-
await this.http.put(
|
|
115
|
-
|
|
116
|
+
const response = await this.http.put<SparkVaultAccount>(apiPath`/account`, updates);
|
|
117
|
+
if (this.config.tokenStorage.setAccount) {
|
|
118
|
+
await this.config.tokenStorage.setAccount(response.data);
|
|
119
|
+
}
|
|
120
|
+
return response.data;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
async revokeRefreshToken(refreshToken?: string): Promise<void> {
|
|
119
124
|
const token = refreshToken ?? await this.config.tokenStorage.getRefreshToken();
|
|
120
125
|
if (!token) return;
|
|
121
126
|
|
|
122
|
-
await this.http.post(
|
|
127
|
+
await this.http.post(apiPath`/auth/logout`, { refresh_token: token }).catch(err => {
|
|
123
128
|
this.config.logger.warn('Logout API call failed', { error: String(err) });
|
|
124
129
|
});
|
|
125
130
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Source of truth: packages/app-sdk/src/email-address.js
|
|
3
|
+
// Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The one email-address grammar: the addresses SparkVault can deliver to, so
|
|
7
|
+
* the only ones a front door may accept. A door that took an address the
|
|
8
|
+
* email transport then refuses answers its caller with a success while the
|
|
9
|
+
* recipient gets nothing (a Notify send, a SparkLink invite, a share), so the
|
|
10
|
+
* accepting doors and the transport ask this one question.
|
|
11
|
+
*
|
|
12
|
+
* An address is read in its canonical form (trimmed, lowercased) and is valid
|
|
13
|
+
* when it has an RFC 5322 dot-atom-text local part in ASCII, exactly one `@`,
|
|
14
|
+
* a domain of at least two labels of letters, digits and inner hyphens, and
|
|
15
|
+
* fits the RFC 5321 lengths (address 320, local part 64, domain 255).
|
|
16
|
+
*
|
|
17
|
+
* The canonical form is also the one every stored email key is spelled in:
|
|
18
|
+
* Identity's directory normalizes an email identifier through
|
|
19
|
+
* canonicalEmailAddress, and Core keys its account and login-claim rows and
|
|
20
|
+
* compares a signed-in address through it, so one spelling of an address
|
|
21
|
+
* finds every row another spelling wrote.
|
|
22
|
+
*
|
|
23
|
+
* Pure and import-free, so every runtime that decides deliverability loads it
|
|
24
|
+
* alike.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const LOCAL_PART = /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+$/;
|
|
28
|
+
const DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
29
|
+
|
|
30
|
+
/** RFC 5321 length limits, in characters. */
|
|
31
|
+
export const EMAIL_ADDRESS_LIMITS = Object.freeze({
|
|
32
|
+
ADDRESS: 320,
|
|
33
|
+
LOCAL_PART: 64,
|
|
34
|
+
DOMAIN: 255
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Why an address is not valid, as emailAddressProblem reports it. */
|
|
38
|
+
export const EMAIL_ADDRESS_PROBLEMS = Object.freeze({
|
|
39
|
+
FORMAT: 'format',
|
|
40
|
+
TOO_LONG: 'too_long',
|
|
41
|
+
LOCAL_PART_TOO_LONG: 'local_part_too_long',
|
|
42
|
+
DOMAIN_TOO_LONG: 'domain_too_long'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The canonical form of an address: trimmed and lowercased.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} value
|
|
49
|
+
* @returns {string}
|
|
50
|
+
* @throws {TypeError} when `value` is not a string: it has no canonical form,
|
|
51
|
+
* and a caller that would key or compare by one must not carry on without it
|
|
52
|
+
*/
|
|
53
|
+
export function canonicalEmailAddress(value) {
|
|
54
|
+
if (typeof value !== 'string') {
|
|
55
|
+
throw new TypeError('An email address is a string');
|
|
56
|
+
}
|
|
57
|
+
return value.trim().toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What makes `value` an address SparkVault cannot deliver to, or null when
|
|
62
|
+
* nothing does.
|
|
63
|
+
*
|
|
64
|
+
* @param {*} value
|
|
65
|
+
* @returns {string|null} One of EMAIL_ADDRESS_PROBLEMS, or null
|
|
66
|
+
*/
|
|
67
|
+
export function emailAddressProblem(value) {
|
|
68
|
+
if (typeof value !== 'string') return EMAIL_ADDRESS_PROBLEMS.FORMAT;
|
|
69
|
+
const address = canonicalEmailAddress(value);
|
|
70
|
+
const at = address.indexOf('@');
|
|
71
|
+
if (at <= 0 || at !== address.lastIndexOf('@')) return EMAIL_ADDRESS_PROBLEMS.FORMAT;
|
|
72
|
+
|
|
73
|
+
const localPart = address.slice(0, at);
|
|
74
|
+
const domain = address.slice(at + 1);
|
|
75
|
+
const labels = domain.split('.');
|
|
76
|
+
if (!LOCAL_PART.test(localPart) || labels.length < 2 || !labels.every((label) => DOMAIN_LABEL.test(label))) {
|
|
77
|
+
return EMAIL_ADDRESS_PROBLEMS.FORMAT;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (address.length > EMAIL_ADDRESS_LIMITS.ADDRESS) return EMAIL_ADDRESS_PROBLEMS.TOO_LONG;
|
|
81
|
+
if (localPart.length > EMAIL_ADDRESS_LIMITS.LOCAL_PART) return EMAIL_ADDRESS_PROBLEMS.LOCAL_PART_TOO_LONG;
|
|
82
|
+
if (domain.length > EMAIL_ADDRESS_LIMITS.DOMAIN) return EMAIL_ADDRESS_PROBLEMS.DOMAIN_TOO_LONG;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether SparkVault can deliver to `value`.
|
|
88
|
+
*
|
|
89
|
+
* @param {*} value
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
export function isValidEmailAddress(value) {
|
|
93
|
+
return emailAddressProblem(value) === null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Source of truth: packages/app-sdk/src/encoding.js
|
|
3
|
+
// Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base64 and base64url (RFC 4648 §4 and §5): the platform's one codec.
|
|
7
|
+
*
|
|
8
|
+
* Dependency-free, and the codec uses no runtime globals (no Buffer, atob or
|
|
9
|
+
* btoa), so the same code runs on the server, in browsers and in React Native:
|
|
10
|
+
* sdk-js bundles this module, and sdk-mobile (which publishes standalone)
|
|
11
|
+
* carries a build-time copy pinned byte-for-byte by its drift test.
|
|
12
|
+
*
|
|
13
|
+
* - `bytesToBase64` writes the standard alphabet with `=` padding;
|
|
14
|
+
* `bytesToBase64url` writes the URL-safe alphabet without padding.
|
|
15
|
+
* - `base64ToBytes` and `base64urlToBytes` each read one alphabet, by the WHATWG
|
|
16
|
+
* forgiving-base64 rules `atob` implements: ASCII whitespace is ignored,
|
|
17
|
+
* padding is optional but, when present, is one or two `=` completing the
|
|
18
|
+
* final quartet, and bits past the last whole byte are dropped. Any other
|
|
19
|
+
* character (the other alphabet's included), `=` anywhere else, or a single
|
|
20
|
+
* character left over after the last whole quartet refuses the text.
|
|
21
|
+
* - `{ canonical: true }` accepts only the exact text the matching encoder
|
|
22
|
+
* writes (no whitespace, the encoder's padding, zero trailing bits), for a
|
|
23
|
+
* value whose spelling is itself compared or signed.
|
|
24
|
+
*
|
|
25
|
+
* - `base64EncodeUtf8` writes standard base64 of a string's UTF-8 bytes, the
|
|
26
|
+
* one form that carries any Unicode through an HTTP header value (a header
|
|
27
|
+
* holds latin1 bytes only); `base64DecodeUtf8` reads back exactly what it
|
|
28
|
+
* writes. These two use TextEncoder and TextDecoder, which Node and every
|
|
29
|
+
* browser provide; sdk-mobile converts UTF-8 itself for the React Native
|
|
30
|
+
* engines that need not.
|
|
31
|
+
* - `isUint8Array` is the platform's one Uint8Array test; every app-sdk check
|
|
32
|
+
* for a Uint8Array applies it.
|
|
33
|
+
*
|
|
34
|
+
* A refused text decodes to null, never to other bytes: each caller raises its
|
|
35
|
+
* own error at its own trust boundary.
|
|
36
|
+
*
|
|
37
|
+
* A page that cannot import an ES module runs this module's own codec
|
|
38
|
+
* (ENCODING_SCRIPT).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const PAD = 0x3d; // '='
|
|
42
|
+
|
|
43
|
+
/** Characters per String.fromCharCode call: far below every engine's argument limit. */
|
|
44
|
+
const CHAR_CODE_CHUNK = 0x2000;
|
|
45
|
+
|
|
46
|
+
function alphabet(characters, padded) {
|
|
47
|
+
const codes = new Uint8Array(64);
|
|
48
|
+
const values = new Int8Array(128).fill(-1);
|
|
49
|
+
for (let index = 0; index < 64; index += 1) {
|
|
50
|
+
codes[index] = characters.charCodeAt(index);
|
|
51
|
+
values[codes[index]] = index;
|
|
52
|
+
}
|
|
53
|
+
return { codes, values, padded };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const BASE64_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
57
|
+
const BASE64URL_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
58
|
+
|
|
59
|
+
const BASE64 = alphabet(BASE64_CHARACTERS, true);
|
|
60
|
+
const BASE64URL = alphabet(BASE64URL_CHARACTERS, false);
|
|
61
|
+
|
|
62
|
+
/** Tab, line feed, form feed, carriage return or space. */
|
|
63
|
+
function isAsciiWhitespace(code) {
|
|
64
|
+
return code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d || code === 0x20;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether a value is a Uint8Array from any realm (a browser frame, a test
|
|
69
|
+
* DOM), a Node Buffer included: the platform's one Uint8Array test, since a
|
|
70
|
+
* realm's own `instanceof Uint8Array` refuses another realm's arrays.
|
|
71
|
+
* @param {unknown} value
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function isUint8Array(value) {
|
|
75
|
+
return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === '[object Uint8Array]';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fromCharCodes(codes) {
|
|
79
|
+
let text = '';
|
|
80
|
+
for (let start = 0; start < codes.length; start += CHAR_CODE_CHUNK) {
|
|
81
|
+
text += String.fromCharCode.apply(null, codes.subarray(start, start + CHAR_CODE_CHUNK));
|
|
82
|
+
}
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function encode(bytes, table) {
|
|
87
|
+
if (!isUint8Array(bytes)) {
|
|
88
|
+
throw new TypeError('Base64 encoding takes a Uint8Array');
|
|
89
|
+
}
|
|
90
|
+
const { codes } = table;
|
|
91
|
+
const tail = bytes.length % 3;
|
|
92
|
+
const whole = bytes.length - tail;
|
|
93
|
+
let tailLength = 0;
|
|
94
|
+
if (tail > 0) tailLength = table.padded ? 4 : tail + 1;
|
|
95
|
+
const chars = new Uint8Array((whole / 3) * 4 + tailLength);
|
|
96
|
+
|
|
97
|
+
let out = 0;
|
|
98
|
+
for (let index = 0; index < whole; index += 3) {
|
|
99
|
+
const group = (bytes[index] << 16) | (bytes[index + 1] << 8) | bytes[index + 2];
|
|
100
|
+
chars[out++] = codes[group >> 18];
|
|
101
|
+
chars[out++] = codes[(group >> 12) & 63];
|
|
102
|
+
chars[out++] = codes[(group >> 6) & 63];
|
|
103
|
+
chars[out++] = codes[group & 63];
|
|
104
|
+
}
|
|
105
|
+
if (tail > 0) {
|
|
106
|
+
const group = (bytes[whole] << 16) | (tail === 2 ? bytes[whole + 1] << 8 : 0);
|
|
107
|
+
chars[out++] = codes[group >> 18];
|
|
108
|
+
chars[out++] = codes[(group >> 12) & 63];
|
|
109
|
+
if (tail === 2) chars[out++] = codes[(group >> 6) & 63];
|
|
110
|
+
chars.fill(PAD, out);
|
|
111
|
+
}
|
|
112
|
+
return fromCharCodes(chars);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function decode(text, table, canonical) {
|
|
116
|
+
if (typeof text !== 'string') return null;
|
|
117
|
+
const { values } = table;
|
|
118
|
+
|
|
119
|
+
// First pass: refuse anything malformed and count the digits, so the output
|
|
120
|
+
// is allocated once at its exact size (multi-megabyte reads decode here).
|
|
121
|
+
let digits = 0;
|
|
122
|
+
let padding = 0;
|
|
123
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
124
|
+
const code = text.charCodeAt(index);
|
|
125
|
+
if (isAsciiWhitespace(code)) {
|
|
126
|
+
if (canonical) return null;
|
|
127
|
+
} else if (code === PAD) {
|
|
128
|
+
padding += 1;
|
|
129
|
+
} else if (padding > 0 || code > 127 || values[code] < 0) {
|
|
130
|
+
return null;
|
|
131
|
+
} else {
|
|
132
|
+
digits += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const remainder = digits % 4;
|
|
136
|
+
if (remainder === 1) return null;
|
|
137
|
+
if (padding > 0 && (padding > 2 || (digits + padding) % 4 !== 0)) return null;
|
|
138
|
+
if (canonical && padding !== (table.padded ? (4 - remainder) % 4 : 0)) return null;
|
|
139
|
+
|
|
140
|
+
const bytes = new Uint8Array((digits * 3) >> 2);
|
|
141
|
+
let buffer = 0;
|
|
142
|
+
let bits = 0;
|
|
143
|
+
let out = 0;
|
|
144
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
145
|
+
const code = text.charCodeAt(index);
|
|
146
|
+
if (code === PAD) break;
|
|
147
|
+
if (isAsciiWhitespace(code)) continue;
|
|
148
|
+
buffer = ((buffer << 6) | values[code]) & 0xfff;
|
|
149
|
+
bits += 6;
|
|
150
|
+
if (bits >= 8) {
|
|
151
|
+
bits -= 8;
|
|
152
|
+
bytes[out++] = buffer >> bits;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (canonical && (buffer & ((1 << bits) - 1)) !== 0) return null;
|
|
156
|
+
return bytes;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Encode bytes as standard base64, padded.
|
|
161
|
+
* @param {Uint8Array} bytes
|
|
162
|
+
* @returns {string}
|
|
163
|
+
* @throws {TypeError} when `bytes` is not a Uint8Array
|
|
164
|
+
*/
|
|
165
|
+
export function bytesToBase64(bytes) {
|
|
166
|
+
return encode(bytes, BASE64);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Encode bytes as base64url, unpadded.
|
|
171
|
+
* @param {Uint8Array} bytes
|
|
172
|
+
* @returns {string}
|
|
173
|
+
* @throws {TypeError} when `bytes` is not a Uint8Array
|
|
174
|
+
*/
|
|
175
|
+
export function bytesToBase64url(bytes) {
|
|
176
|
+
return encode(bytes, BASE64URL);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Decode standard base64.
|
|
181
|
+
* @param {unknown} text
|
|
182
|
+
* @param {{ canonical?: boolean }} [options] - `canonical`: accept only what `bytesToBase64` writes
|
|
183
|
+
* @returns {Uint8Array | null} the bytes, or null when the text is not base64
|
|
184
|
+
*/
|
|
185
|
+
export function base64ToBytes(text, { canonical = false } = {}) {
|
|
186
|
+
return decode(text, BASE64, canonical);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Decode base64url.
|
|
191
|
+
* @param {unknown} text
|
|
192
|
+
* @param {{ canonical?: boolean }} [options] - `canonical`: accept only what `bytesToBase64url` writes
|
|
193
|
+
* @returns {Uint8Array | null} the bytes, or null when the text is not base64url
|
|
194
|
+
*/
|
|
195
|
+
export function base64urlToBytes(text, { canonical = false } = {}) {
|
|
196
|
+
return decode(text, BASE64URL, canonical);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Encode a string as standard base64, padded, of its UTF-8 bytes.
|
|
201
|
+
* @param {string} value
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
export function base64EncodeUtf8(value) {
|
|
205
|
+
return bytesToBase64(new TextEncoder().encode(value));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Decode what `base64EncodeUtf8` writes, strictly: canonical standard base64
|
|
210
|
+
* of well-formed UTF-8, so a mangled value is refused rather than read as
|
|
211
|
+
* whatever text a lenient decoder makes of it.
|
|
212
|
+
* @param {unknown} text
|
|
213
|
+
* @returns {string | null} the string, or null when the text is not canonical base64 of UTF-8
|
|
214
|
+
*/
|
|
215
|
+
export function base64DecodeUtf8(text) {
|
|
216
|
+
const bytes = base64ToBytes(text, { canonical: true });
|
|
217
|
+
if (bytes === null) return null;
|
|
218
|
+
try {
|
|
219
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
220
|
+
} catch {
|
|
221
|
+
// A fatal decoder signals bytes that are not UTF-8 only by throwing.
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* This module's codec as the source of a classic browser script, for a page
|
|
228
|
+
* that cannot import an ES module (Identity's passkey popup, the x.sv reveal
|
|
229
|
+
* page): it declares `bytesToBase64`, `bytesToBase64url`, `base64ToBytes` and
|
|
230
|
+
* `base64urlToBytes` and everything they read at the script's top level, built
|
|
231
|
+
* from the functions and constants above, so the page runs this codec, not a
|
|
232
|
+
* copy. A new module-scope binding any of them reads must be added here.
|
|
233
|
+
* Marked pure so a bundle that never reads it (the browser SDK's) drops it.
|
|
234
|
+
*/
|
|
235
|
+
export const ENCODING_SCRIPT = /* @__PURE__ */ [
|
|
236
|
+
`var PAD = ${PAD};`,
|
|
237
|
+
`var CHAR_CODE_CHUNK = ${CHAR_CODE_CHUNK};`,
|
|
238
|
+
alphabet,
|
|
239
|
+
`var BASE64 = alphabet(${JSON.stringify(BASE64_CHARACTERS)}, true);`,
|
|
240
|
+
`var BASE64URL = alphabet(${JSON.stringify(BASE64URL_CHARACTERS)}, false);`,
|
|
241
|
+
isAsciiWhitespace,
|
|
242
|
+
isUint8Array,
|
|
243
|
+
fromCharCodes,
|
|
244
|
+
encode,
|
|
245
|
+
decode,
|
|
246
|
+
bytesToBase64,
|
|
247
|
+
bytesToBase64url,
|
|
248
|
+
base64ToBytes,
|
|
249
|
+
base64urlToBytes
|
|
250
|
+
].join('\n');
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Source of truth: packages/app-sdk/src/entropy-contract.js
|
|
3
|
+
// Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The request contract of the entropy endpoint (POST /v1/entropy/generate):
|
|
7
|
+
* the output formats it serves and how many bytes one request may ask for.
|
|
8
|
+
*
|
|
9
|
+
* Core's route enforces it; the console and both SDKs hold a request to it
|
|
10
|
+
* before sending, so a request the server would refuse is refused where it is
|
|
11
|
+
* made. Dependency-free: sdk-js bundles it and sdk-mobile carries a generated
|
|
12
|
+
* copy.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Output formats the endpoint serves. `bytes` returns a number array; every other format a string. */
|
|
16
|
+
export const ENTROPY_FORMATS = Object.freeze([
|
|
17
|
+
'hex',
|
|
18
|
+
'base64',
|
|
19
|
+
'base64url',
|
|
20
|
+
'alphanumeric',
|
|
21
|
+
'alphanumeric-mixed',
|
|
22
|
+
'password',
|
|
23
|
+
'numeric',
|
|
24
|
+
'uuid',
|
|
25
|
+
'bytes',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** The hardware ceiling on one entropy draw, in bytes. */
|
|
29
|
+
export const MAX_DRAW_BYTES = 1024;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Formats whose characters are drawn from an alphabet by rejection sampling.
|
|
33
|
+
* They open with two raw bytes per output character, so one request's output
|
|
34
|
+
* caps at half a draw. `numeric` rejects so few bytes that it opens with one
|
|
35
|
+
* byte per digit and keeps the full ceiling.
|
|
36
|
+
*/
|
|
37
|
+
export const CHARSET_FORMATS = Object.freeze(['alphanumeric', 'alphanumeric-mixed', 'password']);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {unknown} format
|
|
41
|
+
* @returns {boolean} Whether the endpoint serves this output format
|
|
42
|
+
*/
|
|
43
|
+
export function isEntropyFormat(format) {
|
|
44
|
+
return ENTROPY_FORMATS.includes(format);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The most bytes one request may ask for in a format (`num_bytes` runs from 1
|
|
49
|
+
* to this). A `uuid` always draws 16 bytes, whatever the request names.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} format - One of ENTROPY_FORMATS
|
|
52
|
+
* @returns {number}
|
|
53
|
+
*/
|
|
54
|
+
export function maxBytesFor(format) {
|
|
55
|
+
return CHARSET_FORMATS.includes(format) ? MAX_DRAW_BYTES / 2 : MAX_DRAW_BYTES;
|
|
56
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// GENERATED FILE — do not edit by hand.
|
|
2
|
+
// Source of truth: packages/app-sdk/src/filename.js
|
|
3
|
+
// Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The one rule for which characters a file name may hold, shared by every
|
|
7
|
+
* runtime that stores a name (Core refuses them at ingest, and clients check
|
|
8
|
+
* before they send) and every runtime that saves a stored name to a device
|
|
9
|
+
* (Forge's Content-Disposition, the web console's download, the x.sv reveal
|
|
10
|
+
* page, the mobile share sheet). A download saves under the file's current
|
|
11
|
+
* name by this rule, or under SAVED_FILENAME_FALLBACK when it has none.
|
|
12
|
+
*
|
|
13
|
+
* UNSAFE characters are never stored, and are replaced in any name saved to a
|
|
14
|
+
* device: path separators, control characters (C0, DEL and C1), and the
|
|
15
|
+
* invisible bidirectional controls that make a name display a different
|
|
16
|
+
* extension than it has (`invoice<U+202E>fdp.exe` shows as `invoiceexe.pdf`).
|
|
17
|
+
*
|
|
18
|
+
* NON-PORTABLE characters (`: * ? " < > |`) are refused by the file systems a
|
|
19
|
+
* name is saved to, so they are never stored either. A saved name keeps them:
|
|
20
|
+
* a stored name cannot hold one, and Forge carries a name's exact text in the
|
|
21
|
+
* RFC 5987 `filename*` parameter.
|
|
22
|
+
*
|
|
23
|
+
* Pure and import-free, so a browser, a container and a Lambda load it alike;
|
|
24
|
+
* sdk-mobile mirrors it at build time (scripts/generate-app-sdk-mirrors.mjs).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Path separators, control characters and bidirectional text controls. */
|
|
28
|
+
const UNSAFE_FILENAME_CHARACTERS = /[\\/\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
|
|
29
|
+
|
|
30
|
+
/** Characters the file systems a name is saved to refuse. */
|
|
31
|
+
const NON_PORTABLE_FILENAME_CHARACTERS = /[:*?"<>|]/;
|
|
32
|
+
|
|
33
|
+
/** Why a name cannot be stored, as storableFilenameProblem reports it. */
|
|
34
|
+
export const FILENAME_PROBLEMS = Object.freeze({
|
|
35
|
+
UNSAFE_CHARACTER: 'unsafe_character',
|
|
36
|
+
NON_PORTABLE_CHARACTER: 'non_portable_character',
|
|
37
|
+
ONLY_DOTS_OR_SPACES: 'only_dots_or_spaces'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const ONLY_DOTS_OR_SPACES = /^[\s.]+$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What stops `name` from being stored as a file name, or null when nothing
|
|
44
|
+
* does. Length and emptiness are the storing runtime's own limits.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @returns {string|null} One of FILENAME_PROBLEMS, or null
|
|
48
|
+
*/
|
|
49
|
+
export function storableFilenameProblem(name) {
|
|
50
|
+
if (UNSAFE_FILENAME_CHARACTERS.test(name)) return FILENAME_PROBLEMS.UNSAFE_CHARACTER;
|
|
51
|
+
if (NON_PORTABLE_FILENAME_CHARACTERS.test(name)) return FILENAME_PROBLEMS.NON_PORTABLE_CHARACTER;
|
|
52
|
+
if (ONLY_DOTS_OR_SPACES.test(name)) return FILENAME_PROBLEMS.ONLY_DOTS_OR_SPACES;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A name as it is saved to a device: every unsafe character replaced by `_`.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} name
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function toSavedFilename(name) {
|
|
63
|
+
return name.replace(new RegExp(UNSAFE_FILENAME_CHARACTERS.source, 'gu'), '_');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The one name a download is saved under when its file has no name. */
|
|
67
|
+
export const SAVED_FILENAME_FALLBACK = 'download';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The name a download is saved under: the file's own name by the saved-name
|
|
71
|
+
* rule (toSavedFilename), or SAVED_FILENAME_FALLBACK when it has none.
|
|
72
|
+
*
|
|
73
|
+
* @param {string|null|undefined} name
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
export function savedFilename(name) {
|
|
77
|
+
return typeof name === 'string' && name !== '' ? toSavedFilename(name) : SAVED_FILENAME_FALLBACK;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The saved-name rule as the source of a classic browser script, for a page
|
|
82
|
+
* that cannot import an ES module (the x.sv reveal page): it declares
|
|
83
|
+
* `savedFilename` and everything it reads at the script's top level, built
|
|
84
|
+
* from the functions and constants above, so the page runs this rule, not a
|
|
85
|
+
* copy. A new module-scope binding the rule reads must be added here.
|
|
86
|
+
*/
|
|
87
|
+
export const SAVED_FILENAME_SCRIPT = [
|
|
88
|
+
`var UNSAFE_FILENAME_CHARACTERS = ${UNSAFE_FILENAME_CHARACTERS};`,
|
|
89
|
+
`var SAVED_FILENAME_FALLBACK = ${JSON.stringify(SAVED_FILENAME_FALLBACK)};`,
|
|
90
|
+
toSavedFilename,
|
|
91
|
+
savedFilename
|
|
92
|
+
].join('\n');
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A storable name derived from text nobody chose as a file name (a note's
|
|
96
|
+
* title, a device's file name): every unsafe or non-portable character
|
|
97
|
+
* replaced by `_`. It can still be only dots or spaces, which a caller that
|
|
98
|
+
* derives a name must refuse or replace itself.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} text
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function toStorableFilename(text) {
|
|
104
|
+
return toSavedFilename(text).replace(new RegExp(NON_PORTABLE_FILENAME_CHARACTERS.source, 'g'), '_');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The longest name an ingot stores, in UTF-16 code units. */
|
|
108
|
+
export const INGOT_NAME_MAX = 255;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Two-part extensions that mark a SparkVault document, by the editor that
|
|
112
|
+
* opens it. A name's extension is the whole marker, so a numbered or
|
|
113
|
+
* shortened copy still opens in its editor ("Plan (2).note.html", never
|
|
114
|
+
* "Plan.note (2).html"); the console and the mobile app classify a document by
|
|
115
|
+
* these lists (./ingot-kind.js).
|
|
116
|
+
*/
|
|
117
|
+
export const WHITEBOARD_MARKER_EXTENSIONS = Object.freeze(['.whiteboard.svg', '.whiteboard.png']);
|
|
118
|
+
export const NOTE_MARKER_EXTENSIONS = Object.freeze(['.note.html', '.note.htm']);
|
|
119
|
+
|
|
120
|
+
/** Every SparkVault document marker. */
|
|
121
|
+
export const DOCUMENT_MARKER_EXTENSIONS = Object.freeze([...WHITEBOARD_MARKER_EXTENSIONS, ...NOTE_MARKER_EXTENSIONS]);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A name's stem and extension: "report.pdf" is { stem: "report", extension:
|
|
125
|
+
* ".pdf" }, "Plan.note.html" is { stem: "Plan", extension: ".note.html" }, and
|
|
126
|
+
* a leading dot is part of the stem.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} name
|
|
129
|
+
* @returns {{ stem: string, extension: string }}
|
|
130
|
+
*/
|
|
131
|
+
export function splitExtension(name) {
|
|
132
|
+
const lowerName = name.toLowerCase();
|
|
133
|
+
const marker = DOCUMENT_MARKER_EXTENSIONS.find(
|
|
134
|
+
(candidate) => lowerName.endsWith(candidate) && lowerName.length > candidate.length
|
|
135
|
+
);
|
|
136
|
+
const dot = marker ? name.length - marker.length : name.lastIndexOf('.');
|
|
137
|
+
return dot > 0
|
|
138
|
+
? { stem: name.slice(0, dot), extension: name.slice(dot) }
|
|
139
|
+
: { stem: name, extension: '' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The longest prefix of `text` within `maxLength` UTF-16 units that splits no
|
|
144
|
+
* character.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} text
|
|
147
|
+
* @param {number} maxLength
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
export function truncateToLength(text, maxLength) {
|
|
151
|
+
let truncated = '';
|
|
152
|
+
for (const character of text) {
|
|
153
|
+
if (truncated.length + character.length > maxLength) break;
|
|
154
|
+
truncated += character;
|
|
155
|
+
}
|
|
156
|
+
return truncated;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* `name` shortened to at most `maxLength` UTF-16 units, keeping its extension:
|
|
161
|
+
* the stem gives way, and a name whose extension leaves no room for a stem is
|
|
162
|
+
* shortened as a whole.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} name
|
|
165
|
+
* @param {number} [maxLength=INGOT_NAME_MAX]
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
export function fitName(name, maxLength = INGOT_NAME_MAX) {
|
|
169
|
+
if (name.length <= maxLength) return name;
|
|
170
|
+
const { stem, extension } = splitExtension(name);
|
|
171
|
+
return extension.length < maxLength
|
|
172
|
+
? `${truncateToLength(stem, maxLength - extension.length)}${extension}`
|
|
173
|
+
: truncateToLength(name, maxLength);
|
|
174
|
+
}
|