@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.
- 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/transfer-retry.d.ts +298 -0
- package/dist/app-sdk/transfer-retry.js +535 -0
- package/dist/app-sdk/transfer-retry.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/upload-publication.d.ts +81 -0
- package/dist/app-sdk/upload-publication.js +93 -0
- package/dist/app-sdk/upload-publication.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 +33 -1
- package/dist/config.js +23 -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 +105 -21
- package/dist/errors.js +74 -28
- 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 +8 -7
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +77 -32
- package/dist/ingots.js +152 -136
- 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 +198 -16
- package/dist/tus.js +816 -327
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +183 -23
- 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/transfer-retry.js +563 -0
- package/src/app-sdk/transient.js +30 -0
- package/src/app-sdk/upload-publication.js +102 -0
- package/src/app-sdk/vat-refusal.js +74 -0
- package/src/billing.ts +16 -25
- package/src/config.ts +56 -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 +135 -38
- 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 +13 -3
- package/src/ingots.ts +225 -180
- 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 +1054 -408
- package/src/types.ts +181 -23
- 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
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
* The request contract of the entropy endpoint (POST /v1/entropy/generate):
|
|
6
|
+
* the output formats it serves and how many bytes one request may ask for.
|
|
7
|
+
*
|
|
8
|
+
* Core's route enforces it; the console and both SDKs hold a request to it
|
|
9
|
+
* before sending, so a request the server would refuse is refused where it is
|
|
10
|
+
* made. Dependency-free: sdk-js bundles it and sdk-mobile carries a generated
|
|
11
|
+
* copy.
|
|
12
|
+
*/
|
|
13
|
+
/** Output formats the endpoint serves. `bytes` returns a number array; every other format a string. */
|
|
14
|
+
export const ENTROPY_FORMATS = Object.freeze([
|
|
15
|
+
'hex',
|
|
16
|
+
'base64',
|
|
17
|
+
'base64url',
|
|
18
|
+
'alphanumeric',
|
|
19
|
+
'alphanumeric-mixed',
|
|
20
|
+
'password',
|
|
21
|
+
'numeric',
|
|
22
|
+
'uuid',
|
|
23
|
+
'bytes',
|
|
24
|
+
]);
|
|
25
|
+
/** The hardware ceiling on one entropy draw, in bytes. */
|
|
26
|
+
export const MAX_DRAW_BYTES = 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Formats whose characters are drawn from an alphabet by rejection sampling.
|
|
29
|
+
* They open with two raw bytes per output character, so one request's output
|
|
30
|
+
* caps at half a draw. `numeric` rejects so few bytes that it opens with one
|
|
31
|
+
* byte per digit and keeps the full ceiling.
|
|
32
|
+
*/
|
|
33
|
+
export const CHARSET_FORMATS = Object.freeze(['alphanumeric', 'alphanumeric-mixed', 'password']);
|
|
34
|
+
/**
|
|
35
|
+
* @param {unknown} format
|
|
36
|
+
* @returns {boolean} Whether the endpoint serves this output format
|
|
37
|
+
*/
|
|
38
|
+
export function isEntropyFormat(format) {
|
|
39
|
+
return ENTROPY_FORMATS.includes(format);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The most bytes one request may ask for in a format (`num_bytes` runs from 1
|
|
43
|
+
* to this). A `uuid` always draws 16 bytes, whatever the request names.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} format - One of ENTROPY_FORMATS
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
export function maxBytesFor(format) {
|
|
49
|
+
return CHARSET_FORMATS.includes(format) ? MAX_DRAW_BYTES / 2 : MAX_DRAW_BYTES;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=entropy-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entropy-contract.js","sourceRoot":"","sources":["../../src/app-sdk/entropy-contract.js"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,4DAA4D;AAC5D,8DAA8D;AAE9D;;;;;;;;GAQG;AAEH,uGAAuG;AACvG,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC;IAC3C,KAAK;IACL,QAAQ;IACR,WAAW;IACX,cAAc;IACd,oBAAoB;IACpB,UAAU;IACV,SAAS;IACT,MAAM;IACN,OAAO;CACR,CAAC,CAAC;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,oBAAoB,EAAE,UAAU,CAAC,CAAC,CAAC;AAEjG;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAM;IACpC,OAAO,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,MAAM;IAChC,OAAO,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;AAChF,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What stops `name` from being stored as a file name, or null when nothing
|
|
3
|
+
* does. Length and emptiness are the storing runtime's own limits.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} name
|
|
6
|
+
* @returns {string|null} One of FILENAME_PROBLEMS, or null
|
|
7
|
+
*/
|
|
8
|
+
export function storableFilenameProblem(name: string): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* A name as it is saved to a device: every unsafe character replaced by `_`.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} name
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function toSavedFilename(name: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* The name a download is saved under: the file's own name by the saved-name
|
|
18
|
+
* rule (toSavedFilename), or SAVED_FILENAME_FALLBACK when it has none.
|
|
19
|
+
*
|
|
20
|
+
* @param {string|null|undefined} name
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function savedFilename(name: string | null | undefined): string;
|
|
24
|
+
/**
|
|
25
|
+
* A storable name derived from text nobody chose as a file name (a note's
|
|
26
|
+
* title, a device's file name): every unsafe or non-portable character
|
|
27
|
+
* replaced by `_`. It can still be only dots or spaces, which a caller that
|
|
28
|
+
* derives a name must refuse or replace itself.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export function toStorableFilename(text: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* A name's stem and extension: "report.pdf" is { stem: "report", extension:
|
|
36
|
+
* ".pdf" }, "Plan.note.html" is { stem: "Plan", extension: ".note.html" }, and
|
|
37
|
+
* a leading dot is part of the stem.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} name
|
|
40
|
+
* @returns {{ stem: string, extension: string }}
|
|
41
|
+
*/
|
|
42
|
+
export function splitExtension(name: string): {
|
|
43
|
+
stem: string;
|
|
44
|
+
extension: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The longest prefix of `text` within `maxLength` UTF-16 units that splits no
|
|
48
|
+
* character.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {number} maxLength
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function truncateToLength(text: string, maxLength: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* `name` shortened to at most `maxLength` UTF-16 units, keeping its extension:
|
|
57
|
+
* the stem gives way, and a name whose extension leaves no room for a stem is
|
|
58
|
+
* shortened as a whole.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @param {number} [maxLength=INGOT_NAME_MAX]
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
export function fitName(name: string, maxLength?: number): string;
|
|
65
|
+
/** Why a name cannot be stored, as storableFilenameProblem reports it. */
|
|
66
|
+
export const FILENAME_PROBLEMS: Readonly<{
|
|
67
|
+
UNSAFE_CHARACTER: "unsafe_character";
|
|
68
|
+
NON_PORTABLE_CHARACTER: "non_portable_character";
|
|
69
|
+
ONLY_DOTS_OR_SPACES: "only_dots_or_spaces";
|
|
70
|
+
}>;
|
|
71
|
+
/** The one name a download is saved under when its file has no name. */
|
|
72
|
+
export const SAVED_FILENAME_FALLBACK: "download";
|
|
73
|
+
/**
|
|
74
|
+
* The saved-name rule as the source of a classic browser script, for a page
|
|
75
|
+
* that cannot import an ES module (the x.sv reveal page): it declares
|
|
76
|
+
* `savedFilename` and everything it reads at the script's top level, built
|
|
77
|
+
* from the functions and constants above, so the page runs this rule, not a
|
|
78
|
+
* copy. A new module-scope binding the rule reads must be added here.
|
|
79
|
+
*/
|
|
80
|
+
export const SAVED_FILENAME_SCRIPT: string;
|
|
81
|
+
/** The longest name an ingot stores, in UTF-16 code units. */
|
|
82
|
+
export const INGOT_NAME_MAX: 255;
|
|
83
|
+
/**
|
|
84
|
+
* Two-part extensions that mark a SparkVault document, by the editor that
|
|
85
|
+
* opens it. A name's extension is the whole marker, so a numbered or
|
|
86
|
+
* shortened copy still opens in its editor ("Plan (2).note.html", never
|
|
87
|
+
* "Plan.note (2).html"); the console and the mobile app classify a document by
|
|
88
|
+
* these lists (./ingot-kind.js).
|
|
89
|
+
*/
|
|
90
|
+
export const WHITEBOARD_MARKER_EXTENSIONS: readonly string[];
|
|
91
|
+
export const NOTE_MARKER_EXTENSIONS: readonly string[];
|
|
92
|
+
/** Every SparkVault document marker. */
|
|
93
|
+
export const DOCUMENT_MARKER_EXTENSIONS: readonly string[];
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
* The one rule for which characters a file name may hold, shared by every
|
|
6
|
+
* runtime that stores a name (Core refuses them at ingest, and clients check
|
|
7
|
+
* before they send) and every runtime that saves a stored name to a device
|
|
8
|
+
* (Forge's Content-Disposition, the web console's download, the x.sv reveal
|
|
9
|
+
* page, the mobile share sheet). A download saves under the file's current
|
|
10
|
+
* name by this rule, or under SAVED_FILENAME_FALLBACK when it has none.
|
|
11
|
+
*
|
|
12
|
+
* UNSAFE characters are never stored, and are replaced in any name saved to a
|
|
13
|
+
* device: path separators, control characters (C0, DEL and C1), and the
|
|
14
|
+
* invisible bidirectional controls that make a name display a different
|
|
15
|
+
* extension than it has (`invoice<U+202E>fdp.exe` shows as `invoiceexe.pdf`).
|
|
16
|
+
*
|
|
17
|
+
* NON-PORTABLE characters (`: * ? " < > |`) are refused by the file systems a
|
|
18
|
+
* name is saved to, so they are never stored either. A saved name keeps them:
|
|
19
|
+
* a stored name cannot hold one, and Forge carries a name's exact text in the
|
|
20
|
+
* RFC 5987 `filename*` parameter.
|
|
21
|
+
*
|
|
22
|
+
* Pure and import-free, so a browser, a container and a Lambda load it alike;
|
|
23
|
+
* sdk-mobile mirrors it at build time (scripts/generate-app-sdk-mirrors.mjs).
|
|
24
|
+
*/
|
|
25
|
+
/** Path separators, control characters and bidirectional text controls. */
|
|
26
|
+
const UNSAFE_FILENAME_CHARACTERS = /[\\/\p{Cc}\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
|
|
27
|
+
/** Characters the file systems a name is saved to refuse. */
|
|
28
|
+
const NON_PORTABLE_FILENAME_CHARACTERS = /[:*?"<>|]/;
|
|
29
|
+
/** Why a name cannot be stored, as storableFilenameProblem reports it. */
|
|
30
|
+
export const FILENAME_PROBLEMS = Object.freeze({
|
|
31
|
+
UNSAFE_CHARACTER: 'unsafe_character',
|
|
32
|
+
NON_PORTABLE_CHARACTER: 'non_portable_character',
|
|
33
|
+
ONLY_DOTS_OR_SPACES: 'only_dots_or_spaces'
|
|
34
|
+
});
|
|
35
|
+
const ONLY_DOTS_OR_SPACES = /^[\s.]+$/;
|
|
36
|
+
/**
|
|
37
|
+
* What stops `name` from being stored as a file name, or null when nothing
|
|
38
|
+
* does. Length and emptiness are the storing runtime's own limits.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @returns {string|null} One of FILENAME_PROBLEMS, or null
|
|
42
|
+
*/
|
|
43
|
+
export function storableFilenameProblem(name) {
|
|
44
|
+
if (UNSAFE_FILENAME_CHARACTERS.test(name))
|
|
45
|
+
return FILENAME_PROBLEMS.UNSAFE_CHARACTER;
|
|
46
|
+
if (NON_PORTABLE_FILENAME_CHARACTERS.test(name))
|
|
47
|
+
return FILENAME_PROBLEMS.NON_PORTABLE_CHARACTER;
|
|
48
|
+
if (ONLY_DOTS_OR_SPACES.test(name))
|
|
49
|
+
return FILENAME_PROBLEMS.ONLY_DOTS_OR_SPACES;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A name as it is saved to a device: every unsafe character replaced by `_`.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function toSavedFilename(name) {
|
|
59
|
+
return name.replace(new RegExp(UNSAFE_FILENAME_CHARACTERS.source, 'gu'), '_');
|
|
60
|
+
}
|
|
61
|
+
/** The one name a download is saved under when its file has no name. */
|
|
62
|
+
export const SAVED_FILENAME_FALLBACK = 'download';
|
|
63
|
+
/**
|
|
64
|
+
* The name a download is saved under: the file's own name by the saved-name
|
|
65
|
+
* rule (toSavedFilename), or SAVED_FILENAME_FALLBACK when it has none.
|
|
66
|
+
*
|
|
67
|
+
* @param {string|null|undefined} name
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
export function savedFilename(name) {
|
|
71
|
+
return typeof name === 'string' && name !== '' ? toSavedFilename(name) : SAVED_FILENAME_FALLBACK;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The saved-name rule as the source of a classic browser script, for a page
|
|
75
|
+
* that cannot import an ES module (the x.sv reveal page): it declares
|
|
76
|
+
* `savedFilename` and everything it reads at the script's top level, built
|
|
77
|
+
* from the functions and constants above, so the page runs this rule, not a
|
|
78
|
+
* copy. A new module-scope binding the rule reads must be added here.
|
|
79
|
+
*/
|
|
80
|
+
export const SAVED_FILENAME_SCRIPT = [
|
|
81
|
+
`var UNSAFE_FILENAME_CHARACTERS = ${UNSAFE_FILENAME_CHARACTERS};`,
|
|
82
|
+
`var SAVED_FILENAME_FALLBACK = ${JSON.stringify(SAVED_FILENAME_FALLBACK)};`,
|
|
83
|
+
toSavedFilename,
|
|
84
|
+
savedFilename
|
|
85
|
+
].join('\n');
|
|
86
|
+
/**
|
|
87
|
+
* A storable name derived from text nobody chose as a file name (a note's
|
|
88
|
+
* title, a device's file name): every unsafe or non-portable character
|
|
89
|
+
* replaced by `_`. It can still be only dots or spaces, which a caller that
|
|
90
|
+
* derives a name must refuse or replace itself.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} text
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function toStorableFilename(text) {
|
|
96
|
+
return toSavedFilename(text).replace(new RegExp(NON_PORTABLE_FILENAME_CHARACTERS.source, 'g'), '_');
|
|
97
|
+
}
|
|
98
|
+
/** The longest name an ingot stores, in UTF-16 code units. */
|
|
99
|
+
export const INGOT_NAME_MAX = 255;
|
|
100
|
+
/**
|
|
101
|
+
* Two-part extensions that mark a SparkVault document, by the editor that
|
|
102
|
+
* opens it. A name's extension is the whole marker, so a numbered or
|
|
103
|
+
* shortened copy still opens in its editor ("Plan (2).note.html", never
|
|
104
|
+
* "Plan.note (2).html"); the console and the mobile app classify a document by
|
|
105
|
+
* these lists (./ingot-kind.js).
|
|
106
|
+
*/
|
|
107
|
+
export const WHITEBOARD_MARKER_EXTENSIONS = Object.freeze(['.whiteboard.svg', '.whiteboard.png']);
|
|
108
|
+
export const NOTE_MARKER_EXTENSIONS = Object.freeze(['.note.html', '.note.htm']);
|
|
109
|
+
/** Every SparkVault document marker. */
|
|
110
|
+
export const DOCUMENT_MARKER_EXTENSIONS = Object.freeze([...WHITEBOARD_MARKER_EXTENSIONS, ...NOTE_MARKER_EXTENSIONS]);
|
|
111
|
+
/**
|
|
112
|
+
* A name's stem and extension: "report.pdf" is { stem: "report", extension:
|
|
113
|
+
* ".pdf" }, "Plan.note.html" is { stem: "Plan", extension: ".note.html" }, and
|
|
114
|
+
* a leading dot is part of the stem.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} name
|
|
117
|
+
* @returns {{ stem: string, extension: string }}
|
|
118
|
+
*/
|
|
119
|
+
export function splitExtension(name) {
|
|
120
|
+
const lowerName = name.toLowerCase();
|
|
121
|
+
const marker = DOCUMENT_MARKER_EXTENSIONS.find((candidate) => lowerName.endsWith(candidate) && lowerName.length > candidate.length);
|
|
122
|
+
const dot = marker ? name.length - marker.length : name.lastIndexOf('.');
|
|
123
|
+
return dot > 0
|
|
124
|
+
? { stem: name.slice(0, dot), extension: name.slice(dot) }
|
|
125
|
+
: { stem: name, extension: '' };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The longest prefix of `text` within `maxLength` UTF-16 units that splits no
|
|
129
|
+
* character.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} text
|
|
132
|
+
* @param {number} maxLength
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
export function truncateToLength(text, maxLength) {
|
|
136
|
+
let truncated = '';
|
|
137
|
+
for (const character of text) {
|
|
138
|
+
if (truncated.length + character.length > maxLength)
|
|
139
|
+
break;
|
|
140
|
+
truncated += character;
|
|
141
|
+
}
|
|
142
|
+
return truncated;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `name` shortened to at most `maxLength` UTF-16 units, keeping its extension:
|
|
146
|
+
* the stem gives way, and a name whose extension leaves no room for a stem is
|
|
147
|
+
* shortened as a whole.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} name
|
|
150
|
+
* @param {number} [maxLength=INGOT_NAME_MAX]
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function fitName(name, maxLength = INGOT_NAME_MAX) {
|
|
154
|
+
if (name.length <= maxLength)
|
|
155
|
+
return name;
|
|
156
|
+
const { stem, extension } = splitExtension(name);
|
|
157
|
+
return extension.length < maxLength
|
|
158
|
+
? `${truncateToLength(stem, maxLength - extension.length)}${extension}`
|
|
159
|
+
: truncateToLength(name, maxLength);
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=filename.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filename.js","sourceRoot":"","sources":["../../src/app-sdk/filename.js"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge's own host, the one a platform-issued transfer URL names.
|
|
3
|
+
* @param {string} hostname
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function isForgeHost(hostname: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The URL Core issues for a transfer: Forge's operation endpoint carrying the
|
|
9
|
+
* ISTK.
|
|
10
|
+
*
|
|
11
|
+
* @param {'encrypt'|'decrypt'} operation
|
|
12
|
+
* @param {string} istk
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function forgeTransferUrl(operation: "encrypt" | "decrypt", istk: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Read a Core-issued transfer URL. It must be written in printable ASCII, be
|
|
18
|
+
* https, carry no userinfo, port or fragment, name an allowed host, have the
|
|
19
|
+
* operation as its whole path, and carry exactly one base64url `istk`. Other
|
|
20
|
+
* query parameters (a preview URL's `disposition=inline`) ride along; the query
|
|
21
|
+
* is read the way a form encodes one (`+` is a space, escapes are decoded).
|
|
22
|
+
*
|
|
23
|
+
* A download fetches the URL it read as it was written; an upload is created
|
|
24
|
+
* at `endpoint` with the ISTK in a header.
|
|
25
|
+
*
|
|
26
|
+
* @param {unknown} value
|
|
27
|
+
* @param {Object} options
|
|
28
|
+
* @param {'encrypt'|'decrypt'} options.operation - The transfer the URL must name
|
|
29
|
+
* @param {(hostname: string) => boolean} [options.isAllowedHost] - Forge's own
|
|
30
|
+
* host by default; a published SDK passes its configured allowlist. It is
|
|
31
|
+
* handed the lowercased host name.
|
|
32
|
+
* @returns {{ ok: true, origin: string, endpoint: string, istk: string } | { ok: false, reason: string }}
|
|
33
|
+
* `origin` is the scheme and host; `endpoint` is where a tus upload is created.
|
|
34
|
+
*/
|
|
35
|
+
export function readForgeTransferUrl(value: unknown, { operation, isAllowedHost }: {
|
|
36
|
+
operation: "encrypt" | "decrypt";
|
|
37
|
+
isAllowedHost?: ((hostname: string) => boolean) | undefined;
|
|
38
|
+
}): {
|
|
39
|
+
ok: true;
|
|
40
|
+
origin: string;
|
|
41
|
+
endpoint: string;
|
|
42
|
+
istk: string;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
reason: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Whether a value is exactly the URL Core issues for a transfer
|
|
49
|
+
* (`forgeTransferUrl`), with nothing added or re-spelled. A client deployed
|
|
50
|
+
* with Core may demand this; a published SDK, which must accept what a newer
|
|
51
|
+
* Core adds, reads with readForgeTransferUrl.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} value
|
|
54
|
+
* @param {'encrypt'|'decrypt'} operation
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
export function isIssuedForgeTransferUrl(value: unknown, operation: "encrypt" | "decrypt"): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Place the session a tus create answered with (its `Location`) under the
|
|
60
|
+
* endpoint the session was created at. Every later request carries the ISTK
|
|
61
|
+
* and the file's bytes, so the session must be one segment directly under the
|
|
62
|
+
* endpoint's path on the endpoint's own origin: `<endpoint path>/<session>` as
|
|
63
|
+
* Forge sends it, or the same with the endpoint's origin written in front.
|
|
64
|
+
*
|
|
65
|
+
* @param {unknown} location
|
|
66
|
+
* @param {string} endpoint - An `endpoint` readForgeTransferUrl returned
|
|
67
|
+
* @returns {string|null} The session URL, or null when the answer points anywhere else
|
|
68
|
+
*/
|
|
69
|
+
export function resolveForgeSessionUrl(location: unknown, endpoint: string): string | null;
|
|
70
|
+
/**
|
|
71
|
+
* Read a tus session URL a client kept to resume an upload: a session Forge
|
|
72
|
+
* opened directly under the operation's endpoint on an allowed host, exactly
|
|
73
|
+
* as resolveForgeSessionUrl placed it when the session was created. A resumed
|
|
74
|
+
* session receives the ISTK and the file's bytes, so it is held to the rule a
|
|
75
|
+
* created one was.
|
|
76
|
+
*
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @param {Object} options
|
|
79
|
+
* @param {'encrypt'|'decrypt'} options.operation - The transfer the session belongs to
|
|
80
|
+
* @param {(hostname: string) => boolean} [options.isAllowedHost] - As for readForgeTransferUrl
|
|
81
|
+
* @returns {{ ok: true, url: string } | { ok: false, reason: string }}
|
|
82
|
+
*/
|
|
83
|
+
export function readForgeSessionUrl(value: unknown, { operation, isAllowedHost }: {
|
|
84
|
+
operation: "encrypt" | "decrypt";
|
|
85
|
+
isAllowedHost?: ((hostname: string) => boolean) | undefined;
|
|
86
|
+
}): {
|
|
87
|
+
ok: true;
|
|
88
|
+
url: string;
|
|
89
|
+
} | {
|
|
90
|
+
ok: false;
|
|
91
|
+
reason: string;
|
|
92
|
+
};
|
|
93
|
+
/** Why a transfer URL was refused; each caller raises its own error for the reason. */
|
|
94
|
+
export const FORGE_URL_REFUSAL: Readonly<{
|
|
95
|
+
/** Not a string, or not an absolute URL written in printable ASCII. */
|
|
96
|
+
INVALID: "invalid_url";
|
|
97
|
+
/** Not https: the credential and the bytes would cross the network in the clear. */
|
|
98
|
+
NOT_HTTPS: "not_https";
|
|
99
|
+
/** Userinfo, a port or a fragment, none of which Core ever issues. */
|
|
100
|
+
UNSAFE_PARTS: "unsafe_parts";
|
|
101
|
+
/** A host the caller does not send transfers to, or one that is not a DNS name. */
|
|
102
|
+
HOST: "host_not_allowed";
|
|
103
|
+
/** A path other than the transfer operation's own. */
|
|
104
|
+
OPERATION: "wrong_operation";
|
|
105
|
+
/** No ISTK, more than one, or one that is not base64url. */
|
|
106
|
+
ISTK: "bad_istk";
|
|
107
|
+
}>;
|
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
* Forge transfer URLs: the one reading of the URL Core issues for an ingot
|
|
6
|
+
* transfer, `https://forge.sparkvault.com/{encrypt|decrypt}?istk=…`, and of the
|
|
7
|
+
* session URL a tus upload continues at, whether just created or kept (resumed,
|
|
8
|
+
* probed or terminated).
|
|
9
|
+
*
|
|
10
|
+
* The URL carries the ISTK, a bearer credential for one transfer, and an upload
|
|
11
|
+
* sends the plaintext after it, so every client reads the URL here before a
|
|
12
|
+
* byte or the credential leaves: the Node tus client (forge-tus), both SDKs
|
|
13
|
+
* (sdk-js bundles this module; sdk-mobile carries a generated copy), the
|
|
14
|
+
* console, the Slack app, the HubSpot record tab and the Salesforce record
|
|
15
|
+
* panel (each a generated copy). Core builds the URL with `forgeTransferUrl`.
|
|
16
|
+
*
|
|
17
|
+
* Both readings are string rules over the one shape Core and Forge write, not
|
|
18
|
+
* a URL parser: a React Native host need not provide a WHATWG `URL` (React
|
|
19
|
+
* Native's own resolves relative references and serializes query strings
|
|
20
|
+
* differently), and a shape this strict leaves nothing for a parser to
|
|
21
|
+
* normalize, so a transfer goes to exactly the host and path that was checked.
|
|
22
|
+
*
|
|
23
|
+
* Dependency-free and free of runtime globals.
|
|
24
|
+
*/
|
|
25
|
+
import { SPARKVAULT_FORGE_HOST } from './platform-domains.js';
|
|
26
|
+
/** Why a transfer URL was refused; each caller raises its own error for the reason. */
|
|
27
|
+
export const FORGE_URL_REFUSAL = Object.freeze({
|
|
28
|
+
/** Not a string, or not an absolute URL written in printable ASCII. */
|
|
29
|
+
INVALID: 'invalid_url',
|
|
30
|
+
/** Not https: the credential and the bytes would cross the network in the clear. */
|
|
31
|
+
NOT_HTTPS: 'not_https',
|
|
32
|
+
/** Userinfo, a port or a fragment, none of which Core ever issues. */
|
|
33
|
+
UNSAFE_PARTS: 'unsafe_parts',
|
|
34
|
+
/** A host the caller does not send transfers to, or one that is not a DNS name. */
|
|
35
|
+
HOST: 'host_not_allowed',
|
|
36
|
+
/** A path other than the transfer operation's own. */
|
|
37
|
+
OPERATION: 'wrong_operation',
|
|
38
|
+
/** No ISTK, more than one, or one that is not base64url. */
|
|
39
|
+
ISTK: 'bad_istk',
|
|
40
|
+
});
|
|
41
|
+
/** An ISTK is base64url; its length is Core's to choose, so it is not pinned. */
|
|
42
|
+
const ISTK_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
43
|
+
/** A tus session Forge opens under its endpoint (`tus_<hex>`): one segment, no dot segment or escape. */
|
|
44
|
+
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
45
|
+
/** Printable ASCII with no space: every character a transfer URL is written in. */
|
|
46
|
+
const URL_TEXT = /^[\x21-\x7e]+$/;
|
|
47
|
+
/** An absolute URL cut at its delimiters: scheme, authority, path, query, fragment. */
|
|
48
|
+
const ABSOLUTE_URL = /^([A-Za-z][A-Za-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(?:\?([^#]*))?(#.*)?$/;
|
|
49
|
+
/** A DNS name: letter-digit-hyphen labels joined by dots (compared lowercased). */
|
|
50
|
+
const HOSTNAME = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/;
|
|
51
|
+
/**
|
|
52
|
+
* Forge's own host, the one a platform-issued transfer URL names.
|
|
53
|
+
* @param {string} hostname
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function isForgeHost(hostname) {
|
|
57
|
+
return hostname === SPARKVAULT_FORGE_HOST;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The URL Core issues for a transfer: Forge's operation endpoint carrying the
|
|
61
|
+
* ISTK.
|
|
62
|
+
*
|
|
63
|
+
* @param {'encrypt'|'decrypt'} operation
|
|
64
|
+
* @param {string} istk
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
export function forgeTransferUrl(operation, istk) {
|
|
68
|
+
return `https://${SPARKVAULT_FORGE_HOST}/${operation}?istk=${encodeURIComponent(istk)}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read a Core-issued transfer URL. It must be written in printable ASCII, be
|
|
72
|
+
* https, carry no userinfo, port or fragment, name an allowed host, have the
|
|
73
|
+
* operation as its whole path, and carry exactly one base64url `istk`. Other
|
|
74
|
+
* query parameters (a preview URL's `disposition=inline`) ride along; the query
|
|
75
|
+
* is read the way a form encodes one (`+` is a space, escapes are decoded).
|
|
76
|
+
*
|
|
77
|
+
* A download fetches the URL it read as it was written; an upload is created
|
|
78
|
+
* at `endpoint` with the ISTK in a header.
|
|
79
|
+
*
|
|
80
|
+
* @param {unknown} value
|
|
81
|
+
* @param {Object} options
|
|
82
|
+
* @param {'encrypt'|'decrypt'} options.operation - The transfer the URL must name
|
|
83
|
+
* @param {(hostname: string) => boolean} [options.isAllowedHost] - Forge's own
|
|
84
|
+
* host by default; a published SDK passes its configured allowlist. It is
|
|
85
|
+
* handed the lowercased host name.
|
|
86
|
+
* @returns {{ ok: true, origin: string, endpoint: string, istk: string } | { ok: false, reason: string }}
|
|
87
|
+
* `origin` is the scheme and host; `endpoint` is where a tus upload is created.
|
|
88
|
+
*/
|
|
89
|
+
export function readForgeTransferUrl(value, { operation, isAllowedHost = isForgeHost }) {
|
|
90
|
+
const url = readHttpsUrl(value);
|
|
91
|
+
if (!url.ok)
|
|
92
|
+
return url;
|
|
93
|
+
if (!isAllowedHost(url.hostname))
|
|
94
|
+
return refused(FORGE_URL_REFUSAL.HOST);
|
|
95
|
+
if (url.path !== `/${operation}`)
|
|
96
|
+
return refused(FORGE_URL_REFUSAL.OPERATION);
|
|
97
|
+
const istks = queryValues(url.query, 'istk');
|
|
98
|
+
if (istks.length !== 1 || !ISTK_PATTERN.test(istks[0]))
|
|
99
|
+
return refused(FORGE_URL_REFUSAL.ISTK);
|
|
100
|
+
const origin = `https://${url.hostname}`;
|
|
101
|
+
return { ok: true, origin, endpoint: `${origin}/${operation}`, istk: istks[0] };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a value is exactly the URL Core issues for a transfer
|
|
105
|
+
* (`forgeTransferUrl`), with nothing added or re-spelled. A client deployed
|
|
106
|
+
* with Core may demand this; a published SDK, which must accept what a newer
|
|
107
|
+
* Core adds, reads with readForgeTransferUrl.
|
|
108
|
+
*
|
|
109
|
+
* @param {unknown} value
|
|
110
|
+
* @param {'encrypt'|'decrypt'} operation
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isIssuedForgeTransferUrl(value, operation) {
|
|
114
|
+
const read = readForgeTransferUrl(value, { operation });
|
|
115
|
+
return read.ok && value === forgeTransferUrl(operation, read.istk);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Place the session a tus create answered with (its `Location`) under the
|
|
119
|
+
* endpoint the session was created at. Every later request carries the ISTK
|
|
120
|
+
* and the file's bytes, so the session must be one segment directly under the
|
|
121
|
+
* endpoint's path on the endpoint's own origin: `<endpoint path>/<session>` as
|
|
122
|
+
* Forge sends it, or the same with the endpoint's origin written in front.
|
|
123
|
+
*
|
|
124
|
+
* @param {unknown} location
|
|
125
|
+
* @param {string} endpoint - An `endpoint` readForgeTransferUrl returned
|
|
126
|
+
* @returns {string|null} The session URL, or null when the answer points anywhere else
|
|
127
|
+
*/
|
|
128
|
+
export function resolveForgeSessionUrl(location, endpoint) {
|
|
129
|
+
const base = readHttpsUrl(endpoint);
|
|
130
|
+
if (!base.ok || base.query !== undefined || typeof location !== 'string' || !URL_TEXT.test(location)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const origin = `https://${base.hostname}`;
|
|
134
|
+
let path = null;
|
|
135
|
+
if (location.startsWith(`${origin}/`)) {
|
|
136
|
+
path = location.slice(origin.length);
|
|
137
|
+
}
|
|
138
|
+
else if (location.startsWith('/') && !location.startsWith('//')) {
|
|
139
|
+
path = location;
|
|
140
|
+
}
|
|
141
|
+
if (path === null || !path.startsWith(`${base.path}/`))
|
|
142
|
+
return null;
|
|
143
|
+
const session = path.slice(base.path.length + 1);
|
|
144
|
+
return SESSION_ID_PATTERN.test(session) ? `${origin}${base.path}/${session}` : null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read a tus session URL a client kept to resume an upload: a session Forge
|
|
148
|
+
* opened directly under the operation's endpoint on an allowed host, exactly
|
|
149
|
+
* as resolveForgeSessionUrl placed it when the session was created. A resumed
|
|
150
|
+
* session receives the ISTK and the file's bytes, so it is held to the rule a
|
|
151
|
+
* created one was.
|
|
152
|
+
*
|
|
153
|
+
* @param {unknown} value
|
|
154
|
+
* @param {Object} options
|
|
155
|
+
* @param {'encrypt'|'decrypt'} options.operation - The transfer the session belongs to
|
|
156
|
+
* @param {(hostname: string) => boolean} [options.isAllowedHost] - As for readForgeTransferUrl
|
|
157
|
+
* @returns {{ ok: true, url: string } | { ok: false, reason: string }}
|
|
158
|
+
*/
|
|
159
|
+
export function readForgeSessionUrl(value, { operation, isAllowedHost = isForgeHost }) {
|
|
160
|
+
const url = readHttpsUrl(value);
|
|
161
|
+
if (!url.ok)
|
|
162
|
+
return url;
|
|
163
|
+
if (!isAllowedHost(url.hostname))
|
|
164
|
+
return refused(FORGE_URL_REFUSAL.HOST);
|
|
165
|
+
const session = resolveForgeSessionUrl(value, `https://${url.hostname}/${operation}`);
|
|
166
|
+
return session === null ? refused(FORGE_URL_REFUSAL.OPERATION) : { ok: true, url: session };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Cut an https URL into its host name, path and query by string rules alone.
|
|
170
|
+
* @param {unknown} value
|
|
171
|
+
* @returns {{ ok: true, hostname: string, path: string, query: string|undefined } | { ok: false, reason: string }}
|
|
172
|
+
*/
|
|
173
|
+
function readHttpsUrl(value) {
|
|
174
|
+
if (typeof value !== 'string' || !URL_TEXT.test(value))
|
|
175
|
+
return refused(FORGE_URL_REFUSAL.INVALID);
|
|
176
|
+
const parts = ABSOLUTE_URL.exec(value);
|
|
177
|
+
if (!parts)
|
|
178
|
+
return refused(FORGE_URL_REFUSAL.INVALID);
|
|
179
|
+
const [, scheme, authority, path, query, fragment] = parts;
|
|
180
|
+
if (scheme.toLowerCase() !== 'https')
|
|
181
|
+
return refused(FORGE_URL_REFUSAL.NOT_HTTPS);
|
|
182
|
+
if (authority.includes('@') || /:[0-9]*$/.test(authority) || fragment !== undefined) {
|
|
183
|
+
return refused(FORGE_URL_REFUSAL.UNSAFE_PARTS);
|
|
184
|
+
}
|
|
185
|
+
const hostname = authority.toLowerCase();
|
|
186
|
+
if (!HOSTNAME.test(hostname))
|
|
187
|
+
return refused(FORGE_URL_REFUSAL.HOST);
|
|
188
|
+
return { ok: true, hostname, path, query };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Every value of one query parameter, names and values read as a form encodes
|
|
192
|
+
* them: `+` is a space and escapes are decoded (text that is not a valid
|
|
193
|
+
* escape sequence stays as written).
|
|
194
|
+
* @param {string|undefined} query
|
|
195
|
+
* @param {string} name
|
|
196
|
+
* @returns {string[]}
|
|
197
|
+
*/
|
|
198
|
+
function queryValues(query, name) {
|
|
199
|
+
const values = [];
|
|
200
|
+
for (const pair of (query ?? '').split('&')) {
|
|
201
|
+
if (!pair)
|
|
202
|
+
continue;
|
|
203
|
+
const equals = pair.indexOf('=');
|
|
204
|
+
const key = formDecode(equals === -1 ? pair : pair.slice(0, equals));
|
|
205
|
+
if (key === name)
|
|
206
|
+
values.push(equals === -1 ? '' : formDecode(pair.slice(equals + 1)));
|
|
207
|
+
}
|
|
208
|
+
return values;
|
|
209
|
+
}
|
|
210
|
+
function formDecode(text) {
|
|
211
|
+
const spaced = text.replace(/\+/g, ' ');
|
|
212
|
+
try {
|
|
213
|
+
return decodeURIComponent(spaced);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return spaced;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function refused(reason) {
|
|
220
|
+
return { ok: false, reason };
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=forge-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forge-url.js","sourceRoot":"","sources":["../../src/app-sdk/forge-url.js"],"names":[],"mappings":"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"}
|