@sparkvault/sdk-mobile 6.0.2 → 7.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.
Files changed (156) 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/transfer-retry.d.ts +298 -0
  36. package/dist/app-sdk/transfer-retry.js +535 -0
  37. package/dist/app-sdk/transfer-retry.js.map +1 -0
  38. package/dist/app-sdk/transient.d.ts +23 -0
  39. package/dist/app-sdk/transient.js +29 -0
  40. package/dist/app-sdk/transient.js.map +1 -0
  41. package/dist/app-sdk/upload-publication.d.ts +81 -0
  42. package/dist/app-sdk/upload-publication.js +93 -0
  43. package/dist/app-sdk/upload-publication.js.map +1 -0
  44. package/dist/app-sdk/vat-refusal.d.ts +66 -0
  45. package/dist/app-sdk/vat-refusal.js +69 -0
  46. package/dist/app-sdk/vat-refusal.js.map +1 -0
  47. package/dist/billing.d.ts +9 -17
  48. package/dist/billing.js +14 -17
  49. package/dist/billing.js.map +1 -1
  50. package/dist/config.d.ts +33 -1
  51. package/dist/config.js +23 -17
  52. package/dist/config.js.map +1 -1
  53. package/dist/encoding.d.ts +30 -4
  54. package/dist/encoding.js +89 -83
  55. package/dist/encoding.js.map +1 -1
  56. package/dist/entropy.d.ts +25 -4
  57. package/dist/entropy.js +62 -15
  58. package/dist/entropy.js.map +1 -1
  59. package/dist/entropy.test-d.d.ts +40 -0
  60. package/dist/entropy.test-d.js +12 -0
  61. package/dist/entropy.test-d.js.map +1 -0
  62. package/dist/errors.d.ts +105 -21
  63. package/dist/errors.js +74 -28
  64. package/dist/errors.js.map +1 -1
  65. package/dist/folders.js +8 -7
  66. package/dist/folders.js.map +1 -1
  67. package/dist/http.d.ts +57 -15
  68. package/dist/http.js +125 -138
  69. package/dist/http.js.map +1 -1
  70. package/dist/identity-dialog.js +4 -30
  71. package/dist/identity-dialog.js.map +1 -1
  72. package/dist/identity-input.d.ts +39 -0
  73. package/dist/identity-input.js +59 -0
  74. package/dist/identity-input.js.map +1 -0
  75. package/dist/index.d.ts +8 -7
  76. package/dist/index.js +5 -4
  77. package/dist/index.js.map +1 -1
  78. package/dist/ingots.d.ts +77 -32
  79. package/dist/ingots.js +152 -136
  80. package/dist/ingots.js.map +1 -1
  81. package/dist/mutex.d.ts +1 -2
  82. package/dist/mutex.js +0 -6
  83. package/dist/mutex.js.map +1 -1
  84. package/dist/notify.d.ts +7 -2
  85. package/dist/notify.js +12 -15
  86. package/dist/notify.js.map +1 -1
  87. package/dist/notify.test-d.d.ts +3 -2
  88. package/dist/notify.test-d.js +1 -1
  89. package/dist/products/identity.d.ts +5 -0
  90. package/dist/products/identity.js +40 -35
  91. package/dist/products/identity.js.map +1 -1
  92. package/dist/request-path.d.ts +32 -0
  93. package/dist/request-path.js +35 -0
  94. package/dist/request-path.js.map +1 -0
  95. package/dist/sparks.js +16 -15
  96. package/dist/sparks.js.map +1 -1
  97. package/dist/tus.d.ts +198 -16
  98. package/dist/tus.js +816 -327
  99. package/dist/tus.js.map +1 -1
  100. package/dist/types.d.ts +183 -23
  101. package/dist/users.d.ts +12 -5
  102. package/dist/users.js +27 -19
  103. package/dist/users.js.map +1 -1
  104. package/dist/validation.d.ts +20 -5
  105. package/dist/validation.js +29 -27
  106. package/dist/validation.js.map +1 -1
  107. package/dist/vaults.d.ts +34 -83
  108. package/dist/vaults.js +64 -44
  109. package/dist/vaults.js.map +1 -1
  110. package/dist/vaults.test-d.d.ts +22 -0
  111. package/dist/vaults.test-d.js +14 -0
  112. package/dist/vaults.test-d.js.map +1 -0
  113. package/package.json +4 -2
  114. package/src/account.ts +16 -11
  115. package/src/app-sdk/email-address.js +94 -0
  116. package/src/app-sdk/encoding.js +250 -0
  117. package/src/app-sdk/entropy-contract.js +56 -0
  118. package/src/app-sdk/filename.js +174 -0
  119. package/src/app-sdk/forge-url.js +224 -0
  120. package/src/app-sdk/ids.js +71 -0
  121. package/src/app-sdk/ingot-access-log.js +51 -0
  122. package/src/app-sdk/phone.js +139 -0
  123. package/src/app-sdk/platform-domains.js +99 -0
  124. package/src/app-sdk/request-path.js +81 -0
  125. package/src/app-sdk/transfer-retry.js +563 -0
  126. package/src/app-sdk/transient.js +30 -0
  127. package/src/app-sdk/upload-publication.js +102 -0
  128. package/src/app-sdk/vat-refusal.js +74 -0
  129. package/src/billing.ts +16 -25
  130. package/src/config.ts +56 -23
  131. package/src/encoding.ts +91 -103
  132. package/src/entropy.test-d.ts +47 -0
  133. package/src/entropy.ts +86 -20
  134. package/src/errors.ts +135 -38
  135. package/src/folders.ts +8 -7
  136. package/src/http.ts +145 -150
  137. package/src/identity-dialog.tsx +4 -37
  138. package/src/identity-input.ts +70 -0
  139. package/src/index.ts +13 -3
  140. package/src/ingots.ts +225 -180
  141. package/src/mutex.ts +1 -8
  142. package/src/notify.test-d.ts +3 -1
  143. package/src/notify.ts +19 -17
  144. package/src/products/identity.ts +45 -39
  145. package/src/request-path.ts +45 -0
  146. package/src/sparks.ts +16 -13
  147. package/src/tus.ts +1054 -408
  148. package/src/types.ts +181 -23
  149. package/src/users.ts +27 -17
  150. package/src/validation.ts +30 -24
  151. package/src/vaults.test-d.ts +29 -0
  152. package/src/vaults.ts +87 -153
  153. package/dist/platform-domains.generated.d.ts +0 -2
  154. package/dist/platform-domains.generated.js +0 -16
  155. package/dist/platform-domains.generated.js.map +0 -1
  156. package/src/platform-domains.generated.ts +0 -17
@@ -0,0 +1,224 @@
1
+ // GENERATED FILE — do not edit by hand.
2
+ // Source of truth: packages/app-sdk/src/forge-url.js
3
+ // Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
4
+
5
+ /**
6
+ * Forge transfer URLs: the one reading of the URL Core issues for an ingot
7
+ * transfer, `https://forge.sparkvault.com/{encrypt|decrypt}?istk=…`, and of the
8
+ * session URL a tus upload continues at, whether just created or kept (resumed,
9
+ * probed or terminated).
10
+ *
11
+ * The URL carries the ISTK, a bearer credential for one transfer, and an upload
12
+ * sends the plaintext after it, so every client reads the URL here before a
13
+ * byte or the credential leaves: the Node tus client (forge-tus), both SDKs
14
+ * (sdk-js bundles this module; sdk-mobile carries a generated copy), the
15
+ * console, the Slack app, the HubSpot record tab and the Salesforce record
16
+ * panel (each a generated copy). Core builds the URL with `forgeTransferUrl`.
17
+ *
18
+ * Both readings are string rules over the one shape Core and Forge write, not
19
+ * a URL parser: a React Native host need not provide a WHATWG `URL` (React
20
+ * Native's own resolves relative references and serializes query strings
21
+ * differently), and a shape this strict leaves nothing for a parser to
22
+ * normalize, so a transfer goes to exactly the host and path that was checked.
23
+ *
24
+ * Dependency-free and free of runtime globals.
25
+ */
26
+
27
+ import { SPARKVAULT_FORGE_HOST } from './platform-domains.js';
28
+
29
+ /** Why a transfer URL was refused; each caller raises its own error for the reason. */
30
+ export const FORGE_URL_REFUSAL = Object.freeze({
31
+ /** Not a string, or not an absolute URL written in printable ASCII. */
32
+ INVALID: 'invalid_url',
33
+ /** Not https: the credential and the bytes would cross the network in the clear. */
34
+ NOT_HTTPS: 'not_https',
35
+ /** Userinfo, a port or a fragment, none of which Core ever issues. */
36
+ UNSAFE_PARTS: 'unsafe_parts',
37
+ /** A host the caller does not send transfers to, or one that is not a DNS name. */
38
+ HOST: 'host_not_allowed',
39
+ /** A path other than the transfer operation's own. */
40
+ OPERATION: 'wrong_operation',
41
+ /** No ISTK, more than one, or one that is not base64url. */
42
+ ISTK: 'bad_istk',
43
+ });
44
+
45
+ /** An ISTK is base64url; its length is Core's to choose, so it is not pinned. */
46
+ const ISTK_PATTERN = /^[A-Za-z0-9_-]+$/;
47
+
48
+ /** A tus session Forge opens under its endpoint (`tus_<hex>`): one segment, no dot segment or escape. */
49
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
50
+
51
+ /** Printable ASCII with no space: every character a transfer URL is written in. */
52
+ const URL_TEXT = /^[\x21-\x7e]+$/;
53
+
54
+ /** An absolute URL cut at its delimiters: scheme, authority, path, query, fragment. */
55
+ const ABSOLUTE_URL = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(#.*)?$/;
56
+
57
+ /** A DNS name: letter-digit-hyphen labels joined by dots (compared lowercased). */
58
+ const HOSTNAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/;
59
+
60
+ /**
61
+ * Forge's own host, the one a platform-issued transfer URL names.
62
+ * @param {string} hostname
63
+ * @returns {boolean}
64
+ */
65
+ export function isForgeHost(hostname) {
66
+ return hostname === SPARKVAULT_FORGE_HOST;
67
+ }
68
+
69
+ /**
70
+ * The URL Core issues for a transfer: Forge's operation endpoint carrying the
71
+ * ISTK.
72
+ *
73
+ * @param {'encrypt'|'decrypt'} operation
74
+ * @param {string} istk
75
+ * @returns {string}
76
+ */
77
+ export function forgeTransferUrl(operation, istk) {
78
+ return `https://${SPARKVAULT_FORGE_HOST}/${operation}?istk=${encodeURIComponent(istk)}`;
79
+ }
80
+
81
+ /**
82
+ * Read a Core-issued transfer URL. It must be written in printable ASCII, be
83
+ * https, carry no userinfo, port or fragment, name an allowed host, have the
84
+ * operation as its whole path, and carry exactly one base64url `istk`. Other
85
+ * query parameters (a preview URL's `disposition=inline`) ride along; the query
86
+ * is read the way a form encodes one (`+` is a space, escapes are decoded).
87
+ *
88
+ * A download fetches the URL it read as it was written; an upload is created
89
+ * at `endpoint` with the ISTK in a header.
90
+ *
91
+ * @param {unknown} value
92
+ * @param {Object} options
93
+ * @param {'encrypt'|'decrypt'} options.operation - The transfer the URL must name
94
+ * @param {(hostname: string) => boolean} [options.isAllowedHost] - Forge's own
95
+ * host by default; a published SDK passes its configured allowlist. It is
96
+ * handed the lowercased host name.
97
+ * @returns {{ ok: true, origin: string, endpoint: string, istk: string } | { ok: false, reason: string }}
98
+ * `origin` is the scheme and host; `endpoint` is where a tus upload is created.
99
+ */
100
+ export function readForgeTransferUrl(value, { operation, isAllowedHost = isForgeHost }) {
101
+ const url = readHttpsUrl(value);
102
+ if (!url.ok) return url;
103
+ if (!isAllowedHost(url.hostname)) return refused(FORGE_URL_REFUSAL.HOST);
104
+ if (url.path !== `/${operation}`) return refused(FORGE_URL_REFUSAL.OPERATION);
105
+ const istks = queryValues(url.query, 'istk');
106
+ if (istks.length !== 1 || !ISTK_PATTERN.test(istks[0])) return refused(FORGE_URL_REFUSAL.ISTK);
107
+ const origin = `https://${url.hostname}`;
108
+ return { ok: true, origin, endpoint: `${origin}/${operation}`, istk: istks[0] };
109
+ }
110
+
111
+ /**
112
+ * Whether a value is exactly the URL Core issues for a transfer
113
+ * (`forgeTransferUrl`), with nothing added or re-spelled. A client deployed
114
+ * with Core may demand this; a published SDK, which must accept what a newer
115
+ * Core adds, reads with readForgeTransferUrl.
116
+ *
117
+ * @param {unknown} value
118
+ * @param {'encrypt'|'decrypt'} operation
119
+ * @returns {boolean}
120
+ */
121
+ export function isIssuedForgeTransferUrl(value, operation) {
122
+ const read = readForgeTransferUrl(value, { operation });
123
+ return read.ok && value === forgeTransferUrl(operation, read.istk);
124
+ }
125
+
126
+ /**
127
+ * Place the session a tus create answered with (its `Location`) under the
128
+ * endpoint the session was created at. Every later request carries the ISTK
129
+ * and the file's bytes, so the session must be one segment directly under the
130
+ * endpoint's path on the endpoint's own origin: `<endpoint path>/<session>` as
131
+ * Forge sends it, or the same with the endpoint's origin written in front.
132
+ *
133
+ * @param {unknown} location
134
+ * @param {string} endpoint - An `endpoint` readForgeTransferUrl returned
135
+ * @returns {string|null} The session URL, or null when the answer points anywhere else
136
+ */
137
+ export function resolveForgeSessionUrl(location, endpoint) {
138
+ const base = readHttpsUrl(endpoint);
139
+ if (!base.ok || base.query !== undefined || typeof location !== 'string' || !URL_TEXT.test(location)) {
140
+ return null;
141
+ }
142
+ const origin = `https://${base.hostname}`;
143
+ let path = null;
144
+ if (location.startsWith(`${origin}/`)) {
145
+ path = location.slice(origin.length);
146
+ } else if (location.startsWith('/') && !location.startsWith('//')) {
147
+ path = location;
148
+ }
149
+ if (path === null || !path.startsWith(`${base.path}/`)) return null;
150
+ const session = path.slice(base.path.length + 1);
151
+ return SESSION_ID_PATTERN.test(session) ? `${origin}${base.path}/${session}` : null;
152
+ }
153
+
154
+ /**
155
+ * Read a tus session URL a client kept to resume an upload: a session Forge
156
+ * opened directly under the operation's endpoint on an allowed host, exactly
157
+ * as resolveForgeSessionUrl placed it when the session was created. A resumed
158
+ * session receives the ISTK and the file's bytes, so it is held to the rule a
159
+ * created one was.
160
+ *
161
+ * @param {unknown} value
162
+ * @param {Object} options
163
+ * @param {'encrypt'|'decrypt'} options.operation - The transfer the session belongs to
164
+ * @param {(hostname: string) => boolean} [options.isAllowedHost] - As for readForgeTransferUrl
165
+ * @returns {{ ok: true, url: string } | { ok: false, reason: string }}
166
+ */
167
+ export function readForgeSessionUrl(value, { operation, isAllowedHost = isForgeHost }) {
168
+ const url = readHttpsUrl(value);
169
+ if (!url.ok) return url;
170
+ if (!isAllowedHost(url.hostname)) return refused(FORGE_URL_REFUSAL.HOST);
171
+ const session = resolveForgeSessionUrl(value, `https://${url.hostname}/${operation}`);
172
+ return session === null ? refused(FORGE_URL_REFUSAL.OPERATION) : { ok: true, url: session };
173
+ }
174
+
175
+ /**
176
+ * Cut an https URL into its host name, path and query by string rules alone.
177
+ * @param {unknown} value
178
+ * @returns {{ ok: true, hostname: string, path: string, query: string|undefined } | { ok: false, reason: string }}
179
+ */
180
+ function readHttpsUrl(value) {
181
+ if (typeof value !== 'string' || !URL_TEXT.test(value)) return refused(FORGE_URL_REFUSAL.INVALID);
182
+ const parts = ABSOLUTE_URL.exec(value);
183
+ if (!parts) return refused(FORGE_URL_REFUSAL.INVALID);
184
+ const [, scheme, authority, path, query, fragment] = parts;
185
+ if (scheme.toLowerCase() !== 'https') return refused(FORGE_URL_REFUSAL.NOT_HTTPS);
186
+ if (authority.includes('@') || /:[0-9]*$/.test(authority) || fragment !== undefined) {
187
+ return refused(FORGE_URL_REFUSAL.UNSAFE_PARTS);
188
+ }
189
+ const hostname = authority.toLowerCase();
190
+ if (!HOSTNAME.test(hostname)) return refused(FORGE_URL_REFUSAL.HOST);
191
+ return { ok: true, hostname, path, query };
192
+ }
193
+
194
+ /**
195
+ * Every value of one query parameter, names and values read as a form encodes
196
+ * them: `+` is a space and escapes are decoded (text that is not a valid
197
+ * escape sequence stays as written).
198
+ * @param {string|undefined} query
199
+ * @param {string} name
200
+ * @returns {string[]}
201
+ */
202
+ function queryValues(query, name) {
203
+ const values = [];
204
+ for (const pair of (query ?? '').split('&')) {
205
+ if (!pair) continue;
206
+ const equals = pair.indexOf('=');
207
+ const key = formDecode(equals === -1 ? pair : pair.slice(0, equals));
208
+ if (key === name) values.push(equals === -1 ? '' : formDecode(pair.slice(equals + 1)));
209
+ }
210
+ return values;
211
+ }
212
+
213
+ function formDecode(text) {
214
+ const spaced = text.replace(/\+/g, ' ');
215
+ try {
216
+ return decodeURIComponent(spaced);
217
+ } catch {
218
+ return spaced;
219
+ }
220
+ }
221
+
222
+ function refused(reason) {
223
+ return { ok: false, reason };
224
+ }
@@ -0,0 +1,71 @@
1
+ // GENERATED FILE — do not edit by hand.
2
+ // Source of truth: packages/app-sdk/src/ids.js
3
+ // Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
4
+
5
+ /**
6
+ * SparkVault resource ids: the one grammar every surface reads an id by before
7
+ * the id names a request path or a stored file.
8
+ *
9
+ * Core mints an id as its kind's prefix, an underscore and a body: the hex of a
10
+ * UUIDv7 (apps/api lib/utils.js buildPrefixedIdFromBytes), or a hex digest for
11
+ * an invitation and an inbox notification. The grammar admits the kind's
12
+ * prefix, '_' and a body of the path-safe alphabet [A-Za-z0-9_-], 128
13
+ * characters in all, so an id that passes is exactly one request-path segment
14
+ * and one file-name component: it can never name a parent path, carry a
15
+ * separator or a dot-segment, or read as an id of another kind.
16
+ *
17
+ * - `isResourceIdOf(kind, value)`: an id of that kind.
18
+ * - `isResourceId(value)`: an id of any kind, for a check that takes several.
19
+ *
20
+ * Browser-safe and import-free: sdk-js bundles it, sdk-mobile mirrors it, and
21
+ * the apps, the console and the mobile app import it.
22
+ */
23
+
24
+ /** The prefix Core mints each resource kind's id with. */
25
+ export const RESOURCE_ID_PREFIXES = Object.freeze({
26
+ account: 'acc',
27
+ folder: 'fld',
28
+ ingot: 'ing',
29
+ invite: 'inv',
30
+ notification: 'ntf',
31
+ spark: 'spk',
32
+ user: 'usr',
33
+ vault: 'vlt'
34
+ });
35
+
36
+ /** The longest id, prefix included. */
37
+ export const RESOURCE_ID_MAX_LENGTH = 128;
38
+
39
+ const ID_BODY = /^[A-Za-z0-9_-]+$/;
40
+
41
+ /**
42
+ * Whether a value is an id of the given kind.
43
+ *
44
+ * @param {string} kind - A key of RESOURCE_ID_PREFIXES
45
+ * @param {unknown} value
46
+ * @returns {boolean}
47
+ * @throws {TypeError} for a kind the grammar does not name: a misspelled kind
48
+ * would otherwise refuse every id without saying why
49
+ */
50
+ export function isResourceIdOf(kind, value) {
51
+ if (!Object.hasOwn(RESOURCE_ID_PREFIXES, kind)) {
52
+ throw new TypeError(`No resource kind '${kind}'`);
53
+ }
54
+ const prefix = `${RESOURCE_ID_PREFIXES[kind]}_`;
55
+ return typeof value === 'string'
56
+ && value.length <= RESOURCE_ID_MAX_LENGTH
57
+ && value.startsWith(prefix)
58
+ && ID_BODY.test(value.slice(prefix.length));
59
+ }
60
+
61
+ const KINDS = Object.freeze(Object.keys(RESOURCE_ID_PREFIXES));
62
+
63
+ /**
64
+ * Whether a value is an id of any kind.
65
+ *
66
+ * @param {unknown} value
67
+ * @returns {boolean}
68
+ */
69
+ export function isResourceId(value) {
70
+ return KINDS.some((kind) => isResourceIdOf(kind, value));
71
+ }
@@ -0,0 +1,51 @@
1
+ // GENERATED FILE — do not edit by hand.
2
+ // Source of truth: packages/app-sdk/src/ingot-access-log.js
3
+ // Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
4
+
5
+ /**
6
+ * Ingot access logs: which of an ingot's audit events are accesses, and how one
7
+ * audit entry reads as an access. Both SDKs read
8
+ * GET /v1/vaults/{vault_id}/ingots/{ingot_id}/audit-logs through it: sdk-js
9
+ * bundles this module, and sdk-mobile (which publishes standalone) carries a
10
+ * build-time copy pinned byte-for-byte by its drift test.
11
+ *
12
+ * An entry arrives in the server's public audit shape (AuditLogRepository
13
+ * toPublicFormat, annotated by the audit-log controller's actor resolution):
14
+ * `timestamp`, `event_type`, `actor_id`, `actor_type`, `actor_display`,
15
+ * `ip_address`, `user_agent`, `metadata` (null while encrypted), `encrypted`,
16
+ * `vault_id` and `ingot_id`.
17
+ */
18
+
19
+ /**
20
+ * The ingot audit events that are accesses, and what each one is: Core records
21
+ * an access to a file as the download that delivered it, a SparkLink open of
22
+ * the file included.
23
+ */
24
+ const ACCESS_ACTIONS = new Map([
25
+ ['ingot_downloaded', 'download'],
26
+ ]);
27
+
28
+ /** The event types an access-log request asks the server for. */
29
+ export const INGOT_ACCESS_EVENT_TYPES = Object.freeze([...ACCESS_ACTIONS.keys()]);
30
+
31
+ /**
32
+ * One audit entry as an access. The actor reads as the server resolved it (a
33
+ * member's email, an API key's name), else as the identity a SparkLink visitor
34
+ * gave (kept in the entry's metadata), else as the raw actor id.
35
+ *
36
+ * @param {{ timestamp: number, event_type: string, actor_id?: string | null, actor_display?: string,
37
+ * ip_address?: string | null, user_agent?: string | null, metadata?: { identity?: string | null } | null }} entry
38
+ * @returns {{ timestamp: number, action: 'download', identity: string, ip_address: string,
39
+ * user_agent: string } | null} null for an event that is not an access
40
+ */
41
+ export function readIngotAccess(entry) {
42
+ const action = ACCESS_ACTIONS.get(entry.event_type);
43
+ if (action === undefined) return null;
44
+ return {
45
+ timestamp: entry.timestamp,
46
+ action,
47
+ identity: entry.actor_display || entry.metadata?.identity || entry.actor_id || 'Unknown',
48
+ ip_address: entry.ip_address ?? '',
49
+ user_agent: entry.user_agent ?? '',
50
+ };
51
+ }
@@ -0,0 +1,139 @@
1
+ // GENERATED FILE — do not edit by hand.
2
+ // Source of truth: packages/app-sdk/src/phone.js
3
+ // Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
4
+
5
+ /**
6
+ * Phone numbers a person types: how one reads as E.164, and whether a typed
7
+ * identifier is a phone number at all. Pure, with no imports, so a browser or
8
+ * an SDK bundle can carry it: Core reads and checks every phone it is given
9
+ * with it, the console imports it, sdk-js bundles it, and sdk-mobile carries a
10
+ * build-time copy pinned byte-for-byte by its drift test.
11
+ *
12
+ * A '+' ahead of the digits DECLARES the country code, so every digit is kept
13
+ * exactly as given: ten digits after a '+' are a foreign number (+47 Norway,
14
+ * +45 Denmark, +65 Singapore, +354 Iceland …), and reading them as a NANP number
15
+ * would hand an OTP or a bearer link to whoever holds the matching North
16
+ * American line. Only input WITHOUT a '+' may take the NANP shorthand: exactly
17
+ * ten digits, or eleven starting with the '1'. Any other bare digit string
18
+ * declares no country and is refused rather than guessed.
19
+ *
20
+ * A typed number holds ASCII digits, at most one '+' ahead of all of them, and
21
+ * the separators people type: whitespace, parentheses, dots and dashes of any
22
+ * script, and the invisible formatting marks a contacts app wraps a number in.
23
+ * The separators and marks are dropped. Anything else is refused, never
24
+ * dropped: stripping it would fold part of the text into the number and deliver
25
+ * to someone else's line — a letter or a dial-control character (',' and ';'
26
+ * pause, '#' and '*' are keypad tones) carries an extension ("x2", ",89"), a
27
+ * digit of another script ('٧', '7') is a digit the ASCII read would silently
28
+ * lose, and a '+' after a digit declares nothing.
29
+ *
30
+ * An identifier typed where an email address or a phone number is accepted is
31
+ * a phone number only when it holds phone characters only, led by a digit, '+'
32
+ * or '(': an email whose local part starts with a digit ("2024user@gmail.com",
33
+ * "12345678@qq.com") stays an email.
34
+ */
35
+
36
+ /** A '+' before any digit: the caller declared the country code. */
37
+ const DECLARED_COUNTRY_CODE = /^\D*\+/;
38
+
39
+ /** E.164 digits once a country code is declared: 8–15, never a leading 0. */
40
+ const E164_DIGITS = /^[1-9]\d{7,14}$/;
41
+
42
+ /** A canonical E.164 value. */
43
+ const E164 = /^\+[1-9]\d{7,14}$/;
44
+
45
+ /** Invisible formatting marks (bidi embeddings, zero-width characters), in any number. */
46
+ const FORMAT_MARKS = /\p{Cf}/gu;
47
+
48
+ /**
49
+ * Typed phone text once its formatting marks are dropped: ASCII digits and
50
+ * separators, with at most one '+', ahead of every digit.
51
+ */
52
+ const PHONE_TEXT = /^[\s().\p{Pd}]*\+?[\d\s().\p{Pd}]*$/u;
53
+
54
+ /** How phone text starts where an identifier may be either: a digit, '+' or '('. */
55
+ const PHONE_LEAD = /^[+(\d]/;
56
+
57
+ /**
58
+ * @param {string} value
59
+ * @returns {string} The value without its formatting marks
60
+ */
61
+ function withoutFormatMarks(value) {
62
+ return value.replace(FORMAT_MARKS, '');
63
+ }
64
+
65
+ /**
66
+ * Read a typed phone number as E.164.
67
+ *
68
+ * @param {unknown} raw
69
+ * @returns {string|null} E.164 with its leading '+', or null when the input
70
+ * names no dialable number
71
+ */
72
+ export function parsePhone(raw) {
73
+ if (typeof raw !== 'string') return null;
74
+ const text = withoutFormatMarks(raw);
75
+ if (!PHONE_TEXT.test(text)) return null;
76
+ const digits = text.replace(/\D/g, '');
77
+
78
+ if (DECLARED_COUNTRY_CODE.test(text)) {
79
+ return E164_DIGITS.test(digits) ? `+${digits}` : null;
80
+ }
81
+ if (digits.length === 10) return `+1${digits}`;
82
+ if (digits.length === 11 && digits.startsWith('1')) return `+${digits}`;
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * Whether a value is already a canonical E.164 phone number — exactly the
88
+ * values parsePhone returns unchanged.
89
+ *
90
+ * @param {unknown} value
91
+ * @returns {boolean}
92
+ */
93
+ export function isE164Phone(value) {
94
+ return typeof value === 'string' && E164.test(value);
95
+ }
96
+
97
+ /**
98
+ * Whether an identifier typed where either is accepted is an email address or
99
+ * a phone number.
100
+ *
101
+ * @param {unknown} value
102
+ * @returns {'email' | 'phone'}
103
+ */
104
+ export function inferIdentityType(value) {
105
+ if (typeof value !== 'string') return 'email';
106
+ const text = withoutFormatMarks(value).trim();
107
+ return PHONE_LEAD.test(text) && PHONE_TEXT.test(text) ? 'phone' : 'email';
108
+ }
109
+
110
+ /**
111
+ * A phone number as a phone field shows it while it is typed: a NANP number as
112
+ * (XXX) XXX-XXXX; a declared country code, or a digit string that is not a NANP
113
+ * number, keeps every digit as given, so formatting never turns one destination
114
+ * into another. Text holding anything but phone characters (a letter, a
115
+ * dial-control character, a digit of another script) is left as typed, so it
116
+ * reaches parsePhone and is refused there instead of being dropped from the
117
+ * field.
118
+ *
119
+ * @param {string} value
120
+ * @returns {string}
121
+ */
122
+ export function formatPhoneDisplay(value) {
123
+ const text = withoutFormatMarks(value);
124
+ if (!PHONE_TEXT.test(text)) return value;
125
+ const digits = text.replace(/\D/g, '');
126
+
127
+ if (DECLARED_COUNTRY_CODE.test(text) && !(digits.startsWith('1') && digits.length === 11)) {
128
+ return `+${digits}`;
129
+ }
130
+ if (digits.length > 11 || (digits.length === 11 && !digits.startsWith('1'))) {
131
+ return digits;
132
+ }
133
+
134
+ const national = digits.length === 11 ? digits.slice(1) : digits;
135
+ if (national.length === 0) return '';
136
+ if (national.length <= 3) return `(${national}`;
137
+ if (national.length <= 6) return `(${national.slice(0, 3)}) ${national.slice(3)}`;
138
+ return `(${national.slice(0, 3)}) ${national.slice(3, 6)}-${national.slice(6, 10)}`;
139
+ }
@@ -0,0 +1,99 @@
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
+ /**
6
+ * SparkVault-owned domain registry.
7
+ *
8
+ * Keep platform domain decisions centralized: CSP, redirect safety, custom
9
+ * domain reservation, and Identity postMessage trust all depend on the same
10
+ * ownership boundary.
11
+ */
12
+
13
+ export const SPARKVAULT_ROOT_DOMAIN = 'sparkvault.com';
14
+
15
+ /** The person-facing Identity portal host (auth.sv shell + hosted login). */
16
+ export const SPARKVAULT_AUTH_HOST = 'auth.sv';
17
+
18
+ /**
19
+ * The Identity IdP origin's host: it serves the OIDC ceremonies (/authorize,
20
+ * /token), the SDK sign-in page and the Identity API, issues every Identity
21
+ * token (`iss` is https://{host}/{account_id}), and holds the hub session
22
+ * cookie. Core verifies the tokens it issues and the Identity product mints
23
+ * them, so both name it from here. Never the auth.sv portal host.
24
+ */
25
+ export const SPARKVAULT_IDENTITY_HOST = 'auth.sparkvault.com';
26
+
27
+ /** The SparkLinks host: shared Sparks, single-use links and the Messaging recipient pages. */
28
+ export const SPARKVAULT_LINKS_HOST = 'x.sv';
29
+
30
+ /** The durable file-share and Secure Upload Portal host. */
31
+ export const SPARKVAULT_FILES_HOST = 'files.sv';
32
+
33
+ export const SPARKVAULT_PLATFORM_SV_HOSTS = Object.freeze([
34
+ SPARKVAULT_AUTH_HOST,
35
+ SPARKVAULT_LINKS_HOST,
36
+ SPARKVAULT_FILES_HOST,
37
+ 'file.sv',
38
+ 'send.sv',
39
+ 'spark.sv',
40
+ 'by.sv',
41
+ 'at.sv',
42
+ 'db.sv'
43
+ ]);
44
+
45
+ /** The SparkVault console. */
46
+ export const SPARKVAULT_APP_HOST = 'app.sparkvault.com';
47
+
48
+ /** Forge, the streaming encrypt/decrypt service every ingot transfer goes through. */
49
+ export const SPARKVAULT_FORGE_HOST = 'forge.sparkvault.com';
50
+
51
+ const SPARKVAULT_WEBSOCKET_HOSTS = Object.freeze([
52
+ 'ws.sparkvault.com'
53
+ ]);
54
+
55
+ function escapeRegExp(value) {
56
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
57
+ }
58
+
59
+ /**
60
+ * The platform's own hosts as patterns over a URL's hostname: the root domain
61
+ * and its subdomains, and each .sv host exactly (isSparkVaultOwnedHostname).
62
+ * Both SDKs trust a backend-issued transfer URL on these hosts by default;
63
+ * their `allowedDownloadHostPatterns` option replaces the list for another
64
+ * deployment.
65
+ */
66
+ export const SPARKVAULT_OWNED_HOST_PATTERNS = Object.freeze([
67
+ new RegExp(`(^|\\.)${escapeRegExp(SPARKVAULT_ROOT_DOMAIN)}$`, 'i'),
68
+ new RegExp(`^(${SPARKVAULT_PLATFORM_SV_HOSTS.map(escapeRegExp).join('|')})$`, 'i')
69
+ ]);
70
+
71
+ function normalizeHostname(hostname) {
72
+ if (typeof hostname !== 'string') return '';
73
+ return hostname.trim().toLowerCase().replace(/\.$/, '');
74
+ }
75
+
76
+ export function isSparkVaultRootOrSubdomain(hostname) {
77
+ const normalized = normalizeHostname(hostname);
78
+ return normalized === SPARKVAULT_ROOT_DOMAIN || normalized.endsWith(`.${SPARKVAULT_ROOT_DOMAIN}`);
79
+ }
80
+
81
+ export function isSparkVaultPlatformSvHost(hostname) {
82
+ return SPARKVAULT_PLATFORM_SV_HOSTS.includes(normalizeHostname(hostname));
83
+ }
84
+
85
+ export function isSparkVaultOwnedHostname(hostname) {
86
+ return isSparkVaultRootOrSubdomain(hostname) || isSparkVaultPlatformSvHost(hostname);
87
+ }
88
+
89
+ export function isSparkVaultReservedDomain(hostname) {
90
+ return isSparkVaultOwnedHostname(hostname);
91
+ }
92
+
93
+ export function sparkVaultPlatformSvOrigins() {
94
+ return SPARKVAULT_PLATFORM_SV_HOSTS.map((host) => `https://${host}`);
95
+ }
96
+
97
+ export function sparkVaultWebSocketOrigins() {
98
+ return SPARKVAULT_WEBSOCKET_HOSTS.map((host) => `wss://${host}`);
99
+ }
@@ -0,0 +1,81 @@
1
+ // GENERATED FILE — do not edit by hand.
2
+ // Source of truth: packages/app-sdk/src/request-path.js
3
+ // Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
4
+
5
+ /**
6
+ * Request paths: the one rule every SDK applies before a caller-supplied value
7
+ * becomes part of a SparkVault request URL.
8
+ *
9
+ * A request carries the caller's credentials (a Bearer token, an API key, a
10
+ * Vault Access Token), so a value that smuggles '/' or a dot-segment into the
11
+ * path would re-target that credentialed request at another route. The rules:
12
+ *
13
+ * - `encodePathSegment`: every value interpolated into a path is encoded as
14
+ * exactly one segment. Percent-encoding keeps '/', '?' and '#' inside the
15
+ * segment, and the three values no encoding can keep one segment ('', '.'
16
+ * and '..', which URL resolution drops or collapses) are refused.
17
+ * - `buildRequestPath`: the template loop an SDK's tagged path builder runs,
18
+ * so every interpolated value passes `encodePathSegment` by construction.
19
+ * - `queryString`: the one query encoding, which leaves unset values out.
20
+ *
21
+ * A server-issued id is checked for its shape first, by the one resource-id
22
+ * grammar (ids.js).
23
+ *
24
+ * Browser-safe and dependency-free: sdk-js bundles it, and sdk-mobile (which
25
+ * publishes standalone) carries a build-time copy pinned byte-for-byte by its
26
+ * drift test. Each SDK wraps it in a typed front door that throws its own
27
+ * validation error.
28
+ */
29
+
30
+ /**
31
+ * Encode a value as exactly one request-path segment.
32
+ * @param {unknown} value
33
+ * @returns {string | null} the encoded segment, or null when the value cannot
34
+ * be one segment: not a string, empty, '.', '..', or not well-formed UTF-16
35
+ * (a lone surrogate has no percent-encoding).
36
+ */
37
+ export function encodePathSegment(value) {
38
+ if (typeof value !== 'string' || value === '' || value === '.' || value === '..') {
39
+ return null;
40
+ }
41
+ try {
42
+ return encodeURIComponent(value);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Join a tagged template's literals with its values, each value encoded as
50
+ * exactly one path segment.
51
+ * @param {readonly string[]} literals
52
+ * @param {readonly unknown[]} values
53
+ * @returns {string | null} the path, or null when any value cannot be one
54
+ * segment (see `encodePathSegment`).
55
+ */
56
+ export function buildRequestPath(literals, values) {
57
+ let path = literals[0];
58
+ for (let index = 0; index < values.length; index += 1) {
59
+ const segment = encodePathSegment(values[index]);
60
+ if (segment === null) return null;
61
+ path += segment + literals[index + 1];
62
+ }
63
+ return path;
64
+ }
65
+
66
+ /**
67
+ * Encode query parameters, leaving out every unset value (`undefined`, `null`
68
+ * or `''`). Other values are sent as their string form, so `0` and `false`
69
+ * are sent.
70
+ * @param {Readonly<Record<string, string | number | boolean | null | undefined>> | undefined} params
71
+ * @returns {string} `?a=1&b=2`, or '' when no value is set.
72
+ */
73
+ export function queryString(params) {
74
+ const search = new URLSearchParams();
75
+ for (const [key, value] of Object.entries(params ?? {})) {
76
+ if (value === undefined || value === null || value === '') continue;
77
+ search.append(key, String(value));
78
+ }
79
+ const query = search.toString();
80
+ return query ? `?${query}` : '';
81
+ }