@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.
Files changed (148) hide show
  1. package/README.md +5 -5
  2. package/dist/account.d.ts +4 -3
  3. package/dist/account.js +16 -11
  4. package/dist/account.js.map +1 -1
  5. package/dist/app-sdk/email-address.d.ts +37 -0
  6. package/dist/app-sdk/email-address.js +91 -0
  7. package/dist/app-sdk/email-address.js.map +1 -0
  8. package/dist/app-sdk/encoding.d.ts +64 -0
  9. package/dist/app-sdk/encoding.js +245 -0
  10. package/dist/app-sdk/encoding.js.map +1 -0
  11. package/dist/app-sdk/entropy-contract.d.ts +33 -0
  12. package/dist/app-sdk/entropy-contract.js +51 -0
  13. package/dist/app-sdk/entropy-contract.js.map +1 -0
  14. package/dist/app-sdk/filename.d.ts +93 -0
  15. package/dist/app-sdk/filename.js +161 -0
  16. package/dist/app-sdk/filename.js.map +1 -0
  17. package/dist/app-sdk/forge-url.d.ts +107 -0
  18. package/dist/app-sdk/forge-url.js +222 -0
  19. package/dist/app-sdk/forge-url.js.map +1 -0
  20. package/dist/app-sdk/ids.d.ts +48 -0
  21. package/dist/app-sdk/ids.js +65 -0
  22. package/dist/app-sdk/ids.js.map +1 -0
  23. package/dist/app-sdk/ingot-access-log.d.ts +29 -0
  24. package/dist/app-sdk/ingot-access-log.js +49 -0
  25. package/dist/app-sdk/ingot-access-log.js.map +1 -0
  26. package/dist/app-sdk/phone.d.ts +37 -0
  27. package/dist/app-sdk/phone.js +134 -0
  28. package/dist/app-sdk/phone.js.map +1 -0
  29. package/dist/app-sdk/platform-domains.d.ts +41 -0
  30. package/dist/app-sdk/platform-domains.js +82 -0
  31. package/dist/app-sdk/platform-domains.js.map +1 -0
  32. package/dist/app-sdk/request-path.d.ts +49 -0
  33. package/dist/app-sdk/request-path.js +81 -0
  34. package/dist/app-sdk/request-path.js.map +1 -0
  35. package/dist/app-sdk/transient.d.ts +23 -0
  36. package/dist/app-sdk/transient.js +29 -0
  37. package/dist/app-sdk/transient.js.map +1 -0
  38. package/dist/app-sdk/vat-refusal.d.ts +66 -0
  39. package/dist/app-sdk/vat-refusal.js +69 -0
  40. package/dist/app-sdk/vat-refusal.js.map +1 -0
  41. package/dist/billing.d.ts +9 -17
  42. package/dist/billing.js +14 -17
  43. package/dist/billing.js.map +1 -1
  44. package/dist/config.d.ts +7 -1
  45. package/dist/config.js +10 -17
  46. package/dist/config.js.map +1 -1
  47. package/dist/encoding.d.ts +30 -4
  48. package/dist/encoding.js +89 -83
  49. package/dist/encoding.js.map +1 -1
  50. package/dist/entropy.d.ts +25 -4
  51. package/dist/entropy.js +62 -15
  52. package/dist/entropy.js.map +1 -1
  53. package/dist/entropy.test-d.d.ts +40 -0
  54. package/dist/entropy.test-d.js +12 -0
  55. package/dist/entropy.test-d.js.map +1 -0
  56. package/dist/errors.d.ts +67 -17
  57. package/dist/errors.js +70 -27
  58. package/dist/errors.js.map +1 -1
  59. package/dist/folders.js +8 -7
  60. package/dist/folders.js.map +1 -1
  61. package/dist/http.d.ts +57 -15
  62. package/dist/http.js +125 -138
  63. package/dist/http.js.map +1 -1
  64. package/dist/identity-dialog.js +4 -30
  65. package/dist/identity-dialog.js.map +1 -1
  66. package/dist/identity-input.d.ts +39 -0
  67. package/dist/identity-input.js +59 -0
  68. package/dist/identity-input.js.map +1 -0
  69. package/dist/index.d.ts +6 -5
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/ingots.d.ts +51 -18
  73. package/dist/ingots.js +132 -122
  74. package/dist/ingots.js.map +1 -1
  75. package/dist/mutex.d.ts +1 -2
  76. package/dist/mutex.js +0 -6
  77. package/dist/mutex.js.map +1 -1
  78. package/dist/notify.d.ts +7 -2
  79. package/dist/notify.js +12 -15
  80. package/dist/notify.js.map +1 -1
  81. package/dist/notify.test-d.d.ts +3 -2
  82. package/dist/notify.test-d.js +1 -1
  83. package/dist/products/identity.d.ts +5 -0
  84. package/dist/products/identity.js +40 -35
  85. package/dist/products/identity.js.map +1 -1
  86. package/dist/request-path.d.ts +32 -0
  87. package/dist/request-path.js +35 -0
  88. package/dist/request-path.js.map +1 -0
  89. package/dist/sparks.js +16 -15
  90. package/dist/sparks.js.map +1 -1
  91. package/dist/tus.d.ts +19 -2
  92. package/dist/tus.js +86 -35
  93. package/dist/tus.js.map +1 -1
  94. package/dist/types.d.ts +167 -15
  95. package/dist/users.d.ts +12 -5
  96. package/dist/users.js +27 -19
  97. package/dist/users.js.map +1 -1
  98. package/dist/validation.d.ts +20 -5
  99. package/dist/validation.js +29 -27
  100. package/dist/validation.js.map +1 -1
  101. package/dist/vaults.d.ts +34 -83
  102. package/dist/vaults.js +64 -44
  103. package/dist/vaults.js.map +1 -1
  104. package/dist/vaults.test-d.d.ts +22 -0
  105. package/dist/vaults.test-d.js +14 -0
  106. package/dist/vaults.test-d.js.map +1 -0
  107. package/package.json +4 -2
  108. package/src/account.ts +16 -11
  109. package/src/app-sdk/email-address.js +94 -0
  110. package/src/app-sdk/encoding.js +250 -0
  111. package/src/app-sdk/entropy-contract.js +56 -0
  112. package/src/app-sdk/filename.js +174 -0
  113. package/src/app-sdk/forge-url.js +224 -0
  114. package/src/app-sdk/ids.js +71 -0
  115. package/src/app-sdk/ingot-access-log.js +51 -0
  116. package/src/app-sdk/phone.js +139 -0
  117. package/src/app-sdk/platform-domains.js +99 -0
  118. package/src/app-sdk/request-path.js +81 -0
  119. package/src/app-sdk/transient.js +30 -0
  120. package/src/app-sdk/vat-refusal.js +74 -0
  121. package/src/billing.ts +16 -25
  122. package/src/config.ts +16 -23
  123. package/src/encoding.ts +91 -103
  124. package/src/entropy.test-d.ts +47 -0
  125. package/src/entropy.ts +86 -20
  126. package/src/errors.ts +93 -33
  127. package/src/folders.ts +8 -7
  128. package/src/http.ts +145 -150
  129. package/src/identity-dialog.tsx +4 -37
  130. package/src/identity-input.ts +70 -0
  131. package/src/index.ts +11 -3
  132. package/src/ingots.ts +173 -159
  133. package/src/mutex.ts +1 -8
  134. package/src/notify.test-d.ts +3 -1
  135. package/src/notify.ts +19 -17
  136. package/src/products/identity.ts +45 -39
  137. package/src/request-path.ts +45 -0
  138. package/src/sparks.ts +16 -13
  139. package/src/tus.ts +90 -37
  140. package/src/types.ts +165 -15
  141. package/src/users.ts +27 -17
  142. package/src/validation.ts +30 -24
  143. package/src/vaults.test-d.ts +29 -0
  144. package/src/vaults.ts +87 -153
  145. package/dist/platform-domains.generated.d.ts +0 -2
  146. package/dist/platform-domains.generated.js +0 -16
  147. package/dist/platform-domains.generated.js.map +0 -1
  148. 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": "6.0.2",
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-platform-domains.mjs && tsc",
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
- '/auth/identity/verify',
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
- '/auth/complete-signup',
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>('/profile');
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>('/profile', updates);
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>('/account');
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 returns a partial
110
- * settings projection, so re-read the full account to refresh the cache and
111
- * return a complete, canonical SparkVaultAccount.
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('/account', updates);
115
- return this.getAccount();
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('/auth/logout', { refresh_token: token }).catch(err => {
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
+ }