@vrtmrz/livesync-commonlib 0.2.3 → 0.3.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 +46 -8
- package/dist/API/DirectFileManipulatorV2.d.ts +8 -1
- package/dist/API/DirectFileManipulatorV2.js +52 -79
- package/dist/API/DirectFileWatchOwner.d.ts +24 -0
- package/dist/API/DirectFileWatchOwner.js +256 -0
- package/dist/API/processSetting.d.ts +1 -0
- package/dist/API/processSetting.js +3 -3
- package/dist/common/models/setting.const.defaults.js +1 -0
- package/dist/common/models/setting.const.qr.js +1 -0
- package/dist/common/models/setting.policy.d.ts +4 -0
- package/dist/common/models/setting.policy.js +1 -0
- package/dist/common/models/setting.type.d.ts +3 -1
- package/dist/common/models/shared.definition.configNames.js +1 -2
- package/dist/common/models/tweak.definition.d.ts +2 -1
- package/dist/common/pathNamespace.d.ts +4 -0
- package/dist/common/pathNamespace.js +10 -0
- package/dist/common/pathNormalization.d.ts +2 -0
- package/dist/common/pathNormalization.js +7 -2
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/settingConstants.js +4 -0
- package/dist/managers/ChunkFetcher.d.ts +3 -0
- package/dist/managers/ChunkFetcher.js +20 -13
- package/dist/managers/LiveSyncManagers.d.ts +2 -0
- package/dist/managers/LiveSyncManagers.js +1 -0
- package/dist/managers/chunkFetchSource.d.ts +14 -0
- package/dist/managers/chunkFetchSource.js +12 -0
- package/dist/platform/AmbiguousNormalisedPathError.d.ts +16 -0
- package/dist/platform/AmbiguousNormalisedPathError.js +21 -0
- package/dist/platform/StoragePathResolver.d.ts +15 -0
- package/dist/platform/StoragePathResolver.js +113 -0
- package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.d.ts +74 -0
- package/dist/platform/browser/UnicodeResolvingFileSystemAccessStorageAdapter.js +169 -0
- package/dist/platform/browser/index.d.ts +1 -0
- package/dist/platform/browser/index.js +2 -0
- package/dist/platform/browser/storage.d.ts +23 -0
- package/dist/platform/browser/storage.js +138 -0
- package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.d.ts +79 -0
- package/dist/platform/node/UnicodeResolvingNodeStorageAdapter.js +218 -0
- package/dist/platform/node/index.d.ts +1 -0
- package/dist/platform/node/index.js +2 -0
- package/dist/platform/node/storage.d.ts +30 -1
- package/dist/platform/node/storage.js +88 -0
- package/dist/platform/pathComparison.d.ts +53 -0
- package/dist/platform/pathComparison.js +39 -0
- package/dist/platform/pathTypes.d.ts +154 -0
- package/dist/platform/pathTypes.js +0 -0
- package/dist/platform/storagePathResolution.d.ts +13 -0
- package/dist/platform/storagePathResolution.js +9 -0
- package/dist/pouchdb/LiveSyncDBFunctions.js +5 -0
- package/dist/pouchdb/LiveSyncLocalDB.d.ts +7 -1
- package/dist/pouchdb/LiveSyncLocalDB.js +13 -10
- package/dist/pouchdb/pathIdentityCompatibility.d.ts +8 -0
- package/dist/pouchdb/pathIdentityCompatibility.js +19 -1
- package/dist/replication/RemoteResource.d.ts +3 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +5 -3
- package/dist/replication/index.d.ts +2 -0
- package/dist/replication/index.js +2 -0
- package/dist/replication/journal/JournalSyncCore.d.ts +1 -0
- package/dist/replication/journal/JournalSyncCore.js +35 -19
- package/dist/replication/journal/LiveSyncJournalReplicator.d.ts +11 -3
- package/dist/replication/journal/LiveSyncJournalReplicator.js +123 -45
- package/dist/replication/trystero/P2PRoomSessionOwner.js +14 -13
- package/dist/replication/trystero/TrysteroReplicator.d.ts +9 -5
- package/dist/replication/trystero/TrysteroReplicator.js +98 -70
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.d.ts +11 -0
- package/dist/replication/trystero/TrysteroReplicator.pathIdentity.js +72 -0
- package/dist/replication/trystero/p2pReplicatorConfigurationIdentity.js +2 -0
- package/dist/replication/trystero/useP2PReplicatorFeature.js +6 -2
- package/dist/serviceModules/Rebuilder.d.ts +1 -0
- package/dist/serviceModules/Rebuilder.js +35 -6
- package/dist/services/base/FileProcessingService.d.ts +2 -2
- package/dist/services/base/RemoteConnection.d.ts +2 -0
- package/dist/services/base/RemoteService.d.ts +1 -1
- package/dist/services/base/RemoteService.js +13 -5
- package/dist/services/implements/injectable/InjectableVaultService.d.ts +1 -1
- package/dist/services/lib/couchdbHttpCache.d.ts +5 -0
- package/dist/services/lib/couchdbHttpCache.js +36 -0
- package/dist/storage.d.ts +4 -0
- package/dist/storage.js +19 -0
- package/dist/string_and_binary/path.js +4 -7
- package/dist/worker/bgWorker.js +1 -1
- package/docs/adr/2026_11_shared_storage_path_resolution.md +73 -0
- package/docs/conflict-resolution.md +1 -1
- package/docs/design_docs/storage_path_resolution.md +305 -0
- package/docs/development.md +15 -1
- package/docs/platform-storage.md +61 -0
- package/docs/proven-in-use.md +30 -13
- package/docs/releasing.md +27 -0
- package/docs/service-feature-composition.md +7 -1
- package/docs/unicode-path-identity.md +104 -12
- package/package.json +10 -2
|
@@ -28,6 +28,9 @@ import { compatGlobal } from "../../common/coreEnvFunctions.js";
|
|
|
28
28
|
import { computeKeyedId, configuredIdKey } from "../../common/idDerivation.js";
|
|
29
29
|
import { getWebCrypto } from "../../mods.js";
|
|
30
30
|
import { uint8ArrayToHexString } from "../../string_and_binary/convert.js";
|
|
31
|
+
import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
|
|
32
|
+
import { readLocalP2PPathIdentity, readRemoteP2PPathIdentity } from "./TrysteroReplicator.pathIdentity.js";
|
|
33
|
+
import { assertDatabasePathIdentity } from "../../pouchdb/pathIdentityCompatibility.js";
|
|
31
34
|
import {
|
|
32
35
|
fromP2PReplicationWireResult,
|
|
33
36
|
toP2PReplicationWireResult,
|
|
@@ -205,6 +208,11 @@ class TrysteroReplicator {
|
|
|
205
208
|
this._onSetup = false;
|
|
206
209
|
}
|
|
207
210
|
async getTweakSettings(fromPeerId) {
|
|
211
|
+
const currentSettings = this.currentSettings;
|
|
212
|
+
const pathNormalizationVersion = getPathNormalizationVersion(currentSettings);
|
|
213
|
+
if (pathNormalizationVersion === 1) {
|
|
214
|
+
await assertDatabasePathIdentity(this.db, currentSettings, true);
|
|
215
|
+
}
|
|
208
216
|
const allSettings = JSON.parse(JSON.stringify(this.settings));
|
|
209
217
|
for (const key in allSettings) {
|
|
210
218
|
if (key == "encrypt") {
|
|
@@ -223,6 +231,7 @@ class TrysteroReplicator {
|
|
|
223
231
|
const idKey = configuredIdKey({ idDerivationVersion, idDerivationKey: idSettings.idDerivationKey ?? "" });
|
|
224
232
|
return {
|
|
225
233
|
...allSettings,
|
|
234
|
+
pathNormalizationVersion,
|
|
226
235
|
idDerivationVersion,
|
|
227
236
|
idDerivationProof: idKey && idSettings.encrypt && idSettings.usePathObfuscation ? await computeKeyedId(idKey, "peer-agreement", fromPeerId) : ""
|
|
228
237
|
};
|
|
@@ -553,6 +562,28 @@ class TrysteroReplicator {
|
|
|
553
562
|
}
|
|
554
563
|
if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
|
|
555
564
|
const logLevel = showNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
|
565
|
+
const rejection = await this.checkPeerForReplication(remotePeer, logLevel, signal);
|
|
566
|
+
if (rejection) return rejection;
|
|
567
|
+
Logger(`P2P Replicating from ${remotePeer}`, logLevel, "p2p-replicator");
|
|
568
|
+
if (this._replicateFromPeers.has(remotePeer)) {
|
|
569
|
+
Logger(`Replication from ${remotePeer} is already in progress`, logLevel, "p2p-replicator");
|
|
570
|
+
return p2pReplicationFailed(new Error("Replication from this peer is already in progress"));
|
|
571
|
+
}
|
|
572
|
+
this._replicateFromPeers.add(remotePeer);
|
|
573
|
+
this.dispatchStatus();
|
|
574
|
+
try {
|
|
575
|
+
return await this.transferFromPeer(remotePeer, fromStart, logLevel, signal);
|
|
576
|
+
} catch (e) {
|
|
577
|
+
if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
|
|
578
|
+
Logger("Error while P2P replicating", logLevel, "p2p-replicator");
|
|
579
|
+
Logger(e, LOG_LEVEL_VERBOSE);
|
|
580
|
+
return p2pReplicationFailed(e);
|
|
581
|
+
} finally {
|
|
582
|
+
this._replicateFromPeers.delete(remotePeer);
|
|
583
|
+
this.dispatchStatus();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async checkPeerForReplication(remotePeer, logLevel, signal) {
|
|
556
587
|
Logger(`P2P Requesting Authentication to ${remotePeer}`, logLevel, "p2p-replicator");
|
|
557
588
|
const authenticated = signal ? await this.requestAuthenticate(remotePeer, signal) : await this.requestAuthenticate(remotePeer);
|
|
558
589
|
if (authenticated !== true) {
|
|
@@ -572,71 +603,62 @@ class TrysteroReplicator {
|
|
|
572
603
|
Logger("Tweak values are not matched", logLevel, "p2p-replicator");
|
|
573
604
|
return p2pReplicationFailed(new Error("Tweak values are not matched"));
|
|
574
605
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
this.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
Logger(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
606
|
+
return void 0;
|
|
607
|
+
}
|
|
608
|
+
async transferFromPeer(remotePeer, fromStart, logLevel, signal) {
|
|
609
|
+
if (!this.server) {
|
|
610
|
+
throw new Error("Server is not available");
|
|
611
|
+
}
|
|
612
|
+
const connection = this.server.getConnection(remotePeer);
|
|
613
|
+
const remoteDB = connection.getRemoteDB(signal);
|
|
614
|
+
const pathNormalizationVersion = getPathNormalizationVersion(this.currentSettings);
|
|
615
|
+
const [localPathIdentity, remotePathIdentity] = await Promise.all([
|
|
616
|
+
readLocalP2PPathIdentity(this.db, pathNormalizationVersion),
|
|
617
|
+
readRemoteP2PPathIdentity(remoteDB, pathNormalizationVersion)
|
|
618
|
+
]);
|
|
619
|
+
if (localPathIdentity !== pathNormalizationVersion || remotePathIdentity !== pathNormalizationVersion) {
|
|
620
|
+
const error = new Error("P2P database path identity does not match the configured path identity.");
|
|
621
|
+
Logger(error.message, logLevel, "p2p-replicator");
|
|
622
|
+
return p2pReplicationFailed(error);
|
|
623
|
+
}
|
|
624
|
+
Logger(`P2P replicateFrom preparing remote DB info for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
|
|
625
|
+
const remoteDBInfo = await remoteDB.info();
|
|
626
|
+
Logger(
|
|
627
|
+
`P2P replicateFrom remote DB info for ${remotePeer}: ${remoteDBInfo.db_name} seq=${remoteDBInfo.update_seq}`,
|
|
628
|
+
LOG_LEVEL_VERBOSE,
|
|
629
|
+
"p2p-replicator"
|
|
630
|
+
);
|
|
631
|
+
const localDBInfo = await this.db.info();
|
|
632
|
+
Logger(
|
|
633
|
+
`P2P replicateFrom local DB info for ${remotePeer}: ${localDBInfo.db_name} seq=${localDBInfo.update_seq}`,
|
|
634
|
+
LOG_LEVEL_VERBOSE,
|
|
635
|
+
"p2p-replicator"
|
|
636
|
+
);
|
|
637
|
+
Logger(`P2P replicateFrom entering replicateShim for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
|
|
638
|
+
const outcome = await replicateShim(
|
|
639
|
+
this.db,
|
|
640
|
+
remoteDB,
|
|
641
|
+
async (docs, info) => {
|
|
642
|
+
await this._env.processReplicatedDocs(docs);
|
|
643
|
+
void this.dispatchReplicationProgress(remotePeer, info);
|
|
644
|
+
void this.acknowledgeProgress(remotePeer, info);
|
|
645
|
+
void this.notifyProgress(remotePeer);
|
|
646
|
+
Logger(
|
|
647
|
+
`P2P Replication from ${remotePeer}
|
|
612
648
|
${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
Logger(`P2P replicateFrom replicateShim returned for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
|
|
625
|
-
void this.acknowledgeProgress(remotePeer, void 0);
|
|
626
|
-
if (outcome.status === "cancelled") {
|
|
627
|
-
Logger(`P2P Replication from ${remotePeer} has been cancelled`, logLevel, "p2p-replicator");
|
|
628
|
-
return P2P_REPLICATION_CANCELLED;
|
|
629
|
-
}
|
|
630
|
-
Logger(`P2P Replication from ${remotePeer} has been completed`, logLevel, "p2p-replicator");
|
|
631
|
-
} catch (e) {
|
|
632
|
-
if (signal?.aborted) return P2P_REPLICATION_CANCELLED;
|
|
633
|
-
Logger("Error while P2P replicating", logLevel, "p2p-replicator");
|
|
634
|
-
Logger(e, LOG_LEVEL_VERBOSE);
|
|
635
|
-
return p2pReplicationFailed(e);
|
|
636
|
-
} finally {
|
|
637
|
-
this._replicateFromPeers.delete(remotePeer);
|
|
638
|
-
this.dispatchStatus();
|
|
649
|
+
logLevel,
|
|
650
|
+
"p2p-replicator"
|
|
651
|
+
);
|
|
652
|
+
},
|
|
653
|
+
{ live: false, rewind: fromStart, signal }
|
|
654
|
+
);
|
|
655
|
+
Logger(`P2P replicateFrom replicateShim returned for ${remotePeer}`, LOG_LEVEL_VERBOSE, "p2p-replicator");
|
|
656
|
+
void this.acknowledgeProgress(remotePeer, void 0);
|
|
657
|
+
if (outcome.status === "cancelled") {
|
|
658
|
+
Logger(`P2P Replication from ${remotePeer} has been cancelled`, logLevel, "p2p-replicator");
|
|
659
|
+
return P2P_REPLICATION_CANCELLED;
|
|
639
660
|
}
|
|
661
|
+
Logger(`P2P Replication from ${remotePeer} has been completed`, logLevel, "p2p-replicator");
|
|
640
662
|
return P2P_REPLICATION_COMPLETED;
|
|
641
663
|
}
|
|
642
664
|
notifyProgress(excludePeerId) {
|
|
@@ -739,12 +761,6 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
739
761
|
Logger("Peer is not found", logLevel);
|
|
740
762
|
return false;
|
|
741
763
|
}
|
|
742
|
-
if (this.platform === "pseudo-replicator") {
|
|
743
|
-
return true;
|
|
744
|
-
}
|
|
745
|
-
if (peerPlatform === "pseudo-replicator") {
|
|
746
|
-
return true;
|
|
747
|
-
}
|
|
748
764
|
const connection = this.server.getConnection(peerId);
|
|
749
765
|
const challengeBytes = new Uint8Array(16);
|
|
750
766
|
(await getWebCrypto()).getRandomValues(challengeBytes);
|
|
@@ -752,6 +768,15 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
752
768
|
const tweakValues = await connection.invokeRemoteObjectFunction("getTweakSettings", [challenge], 5e3, signal);
|
|
753
769
|
if (signal?.aborted) return false;
|
|
754
770
|
const thisTweakValues = await this.getTweakSettings(challenge);
|
|
771
|
+
const localPathNormalizationVersion = getPathNormalizationVersion(thisTweakValues);
|
|
772
|
+
const peerPathNormalizationVersion = getPathNormalizationVersion(tweakValues);
|
|
773
|
+
if (localPathNormalizationVersion !== peerPathNormalizationVersion) {
|
|
774
|
+
Logger("Replication cancelled: The path identity mode does not match this peer.", logLevel);
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
if (this.platform === "pseudo-replicator" || peerPlatform === "pseudo-replicator") {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
755
780
|
if (thisTweakValues.passphrase !== tweakValues.passphrase) {
|
|
756
781
|
Logger(
|
|
757
782
|
"Replication cancelled: Passphrase is not matched\nCannot replicate to a remote database until the problem is resolved.",
|
|
@@ -760,7 +785,10 @@ ${info.lastSeq} / ${info.maxSeqInBatch})`,
|
|
|
760
785
|
return false;
|
|
761
786
|
}
|
|
762
787
|
if (thisTweakValues.encrypt !== tweakValues.encrypt || thisTweakValues.usePathObfuscation !== tweakValues.usePathObfuscation) {
|
|
763
|
-
Logger(
|
|
788
|
+
Logger(
|
|
789
|
+
"Replication cancelled: The encryption or path obfuscation settings do not match this peer.",
|
|
790
|
+
logLevel
|
|
791
|
+
);
|
|
764
792
|
return false;
|
|
765
793
|
}
|
|
766
794
|
if (thisTweakValues.encrypt && thisTweakValues.usePathObfuscation && ((thisTweakValues.idDerivationVersion ?? 0) !== (tweakValues.idDerivationVersion ?? 0) || (thisTweakValues.idDerivationProof ?? "") !== (tweakValues.idDerivationProof ?? ""))) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PathNormalizationVersion } from "../../common/models/setting.type.js";
|
|
2
|
+
export declare function readLocalP2PPathIdentity(database: {
|
|
3
|
+
get(id: string): Promise<unknown>;
|
|
4
|
+
}, configuredVersion: PathNormalizationVersion): Promise<PathNormalizationVersion>;
|
|
5
|
+
export declare function readRemoteP2PPathIdentity(database: {
|
|
6
|
+
bulkGet(options: {
|
|
7
|
+
docs: {
|
|
8
|
+
id: string;
|
|
9
|
+
}[];
|
|
10
|
+
}): Promise<unknown>;
|
|
11
|
+
}, configuredVersion: PathNormalizationVersion): Promise<PathNormalizationVersion>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { VERSIONING_DOCID } from "../../common/models/db.const.js";
|
|
2
|
+
import {
|
|
3
|
+
assessRemoteFeatureDocument,
|
|
4
|
+
describeRemoteFeatureRejection,
|
|
5
|
+
NFC_PATH_IDENTITY_FEATURE
|
|
6
|
+
} from "../../pouchdb/remoteFeatureCompatibility.js";
|
|
7
|
+
import { isErrorOfMissingDoc } from "../../pouchdb/utils_couchdb.js";
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function hasOwnField(value, key) {
|
|
12
|
+
return Object.hasOwn(value, key);
|
|
13
|
+
}
|
|
14
|
+
function isVersionInfoResponse(value) {
|
|
15
|
+
if (!isRecord(value) || !Array.isArray(value.results) || value.results.length !== 1) return false;
|
|
16
|
+
const [result] = value.results;
|
|
17
|
+
return isRecord(result) && result.id === VERSIONING_DOCID && Array.isArray(result.docs) && result.docs.length === 1 && isRecord(result.docs[0]);
|
|
18
|
+
}
|
|
19
|
+
function isMissingDocumentResult(result) {
|
|
20
|
+
if (!hasOwnField(result, "error")) return false;
|
|
21
|
+
const error = result.error;
|
|
22
|
+
const errorDetails = isRecord(error) ? error : void 0;
|
|
23
|
+
const status = errorDetails?.status ?? result.status;
|
|
24
|
+
const errorName = errorDetails?.name ?? result.name;
|
|
25
|
+
const errorCode = typeof error === "string" ? error : errorDetails?.error;
|
|
26
|
+
return status === 404 || status === void 0 && (errorName === "not_found" || errorCode === "not_found");
|
|
27
|
+
}
|
|
28
|
+
function parseRemotePathIdentityDocument(response) {
|
|
29
|
+
if (!isVersionInfoResponse(response)) {
|
|
30
|
+
throw new Error("P2P remote versioninfo response is malformed.");
|
|
31
|
+
}
|
|
32
|
+
const [documentResult] = response.results[0].docs;
|
|
33
|
+
if (hasOwnField(documentResult, "ok")) {
|
|
34
|
+
const document = documentResult.ok;
|
|
35
|
+
if (!isRecord(document) || document._id !== VERSIONING_DOCID) {
|
|
36
|
+
throw new Error("P2P remote versioninfo response is malformed.");
|
|
37
|
+
}
|
|
38
|
+
return { kind: "found", document };
|
|
39
|
+
}
|
|
40
|
+
if (isMissingDocumentResult(documentResult)) return { kind: "missing" };
|
|
41
|
+
throw new Error("P2P remote versioninfo could not be read.");
|
|
42
|
+
}
|
|
43
|
+
function resolvePathIdentity(result, configuredVersion) {
|
|
44
|
+
if (result.kind === "missing") {
|
|
45
|
+
if (configuredVersion === 0) return 0;
|
|
46
|
+
throw new Error("P2P NFC database is missing path identity version information.");
|
|
47
|
+
}
|
|
48
|
+
const assessment = assessRemoteFeatureDocument(result.document);
|
|
49
|
+
if (assessment.status === "older-generation" && configuredVersion === 0) return 0;
|
|
50
|
+
if (assessment.status !== "supported") {
|
|
51
|
+
throw new Error(describeRemoteFeatureRejection(assessment));
|
|
52
|
+
}
|
|
53
|
+
return assessment.usedFeatures.includes(NFC_PATH_IDENTITY_FEATURE) ? 1 : 0;
|
|
54
|
+
}
|
|
55
|
+
async function readLocalP2PPathIdentity(database, configuredVersion) {
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
result = { kind: "found", document: await database.get(VERSIONING_DOCID) };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (!isErrorOfMissingDoc(error)) throw error;
|
|
61
|
+
result = { kind: "missing" };
|
|
62
|
+
}
|
|
63
|
+
return resolvePathIdentity(result, configuredVersion);
|
|
64
|
+
}
|
|
65
|
+
async function readRemoteP2PPathIdentity(database, configuredVersion) {
|
|
66
|
+
const response = await database.bulkGet({ docs: [{ id: VERSIONING_DOCID }] });
|
|
67
|
+
return resolvePathIdentity(parseRemotePathIdentityDocument(response), configuredVersion);
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
readLocalP2PPathIdentity,
|
|
71
|
+
readRemoteP2PPathIdentity
|
|
72
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
splitP2PRelayUrls,
|
|
8
8
|
splitP2PTurnServerUrls
|
|
9
9
|
} from "../../common/models/setting.p2p.js";
|
|
10
|
+
import { getPathNormalizationVersion } from "../../common/pathNormalization.js";
|
|
10
11
|
function getP2PReplicatorConfigurationIdentity(settings) {
|
|
11
12
|
const managedTurnSelected = hasManagedP2PTurnConfiguration(settings);
|
|
12
13
|
const turnServerValue = settings.P2P_turnServers ?? "";
|
|
@@ -25,6 +26,7 @@ function getP2PReplicatorConfigurationIdentity(settings) {
|
|
|
25
26
|
managedTurnSelected ? [settings.P2P_managedType, settings.P2P_managedId, settings.P2P_managedToken] : void 0,
|
|
26
27
|
normaliseP2PMaxWirePayloadBytes(settings.P2P_maxWirePayloadBytes),
|
|
27
28
|
effectivePath,
|
|
29
|
+
getPathNormalizationVersion(settings),
|
|
28
30
|
settings.P2P_useDiagRTC ?? false
|
|
29
31
|
]);
|
|
30
32
|
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
supportedStopActiveTransfer
|
|
16
16
|
} from "../index.js";
|
|
17
17
|
import { getP2PReplicatorConfigurationIdentity } from "./p2pReplicatorConfigurationIdentity.js";
|
|
18
|
-
import {
|
|
18
|
+
import { assertSupportedPathNormalizationVersion } from "../../common/pathNormalization.js";
|
|
19
19
|
function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFactory, options = {}) {
|
|
20
20
|
const service = createP2PService(
|
|
21
21
|
{
|
|
@@ -63,7 +63,11 @@ function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFa
|
|
|
63
63
|
kind: REMOTE_P2P,
|
|
64
64
|
diagnosticName: "P2P",
|
|
65
65
|
readiness: PEER_REPLICATION_READINESS,
|
|
66
|
-
isConfigured: (settings) =>
|
|
66
|
+
isConfigured: (settings) => {
|
|
67
|
+
if (settings.remoteType !== REMOTE_P2P || !settings.P2P_Enabled) return false;
|
|
68
|
+
assertSupportedPathNormalizationVersion(settings.pathNormalizationVersion);
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
67
71
|
configurationIdentity: getP2PReplicatorConfigurationIdentity,
|
|
68
72
|
create: createP2PReplicator,
|
|
69
73
|
remoteResources: NO_REMOTE_RESOURCE_CAPABILITIES,
|
|
@@ -66,6 +66,7 @@ export declare class ServiceRebuilder extends ServiceModuleBase<ServiceRebuilder
|
|
|
66
66
|
private scheduleInitialisation;
|
|
67
67
|
scheduleRebuild(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
|
68
68
|
scheduleFetch(prepareBeforeRestart?: () => Promise<void>): Promise<boolean>;
|
|
69
|
+
private markRemoteLockedForRebuild;
|
|
69
70
|
private _tryResetRemoteDatabase;
|
|
70
71
|
private _onResetLocalDatabase;
|
|
71
72
|
suspendAllSync(): Promise<void>;
|
|
@@ -26,6 +26,12 @@ const FAST_FETCH_RETRY_DELAYS = [2e3, 5e3, 1e4, 2e4];
|
|
|
26
26
|
function canResetCentralRemote(replicator) {
|
|
27
27
|
return "tryResetRemoteDatabase" in replicator && typeof replicator.tryResetRemoteDatabase === "function";
|
|
28
28
|
}
|
|
29
|
+
function canCheckJournalPathIdentity(replicator) {
|
|
30
|
+
return "assertRemotePathIdentity" in replicator && typeof replicator.assertRemotePathIdentity === "function";
|
|
31
|
+
}
|
|
32
|
+
function canLockJournalForRebuild(replicator) {
|
|
33
|
+
return canResetCentralRemote(replicator) && "markRemoteLockedForRebuild" in replicator && typeof replicator.markRemoteLockedForRebuild === "function";
|
|
34
|
+
}
|
|
29
35
|
class ServiceRebuilder extends ServiceModuleBase {
|
|
30
36
|
events;
|
|
31
37
|
appLifecycle;
|
|
@@ -100,7 +106,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
100
106
|
notifyThresholdOfRemoteStorageSize: DEFAULT_SETTINGS.notifyThresholdOfRemoteStorageSize
|
|
101
107
|
});
|
|
102
108
|
await this.control.applySettings();
|
|
103
|
-
await this.
|
|
109
|
+
await this.markRemoteLockedForRebuild();
|
|
104
110
|
await this._tryResetRemoteDatabase();
|
|
105
111
|
await this.replication.markLocked();
|
|
106
112
|
await delay(500);
|
|
@@ -145,7 +151,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
145
151
|
}
|
|
146
152
|
return;
|
|
147
153
|
}
|
|
148
|
-
await this.
|
|
154
|
+
await this.markRemoteLockedForRebuild();
|
|
149
155
|
await this._tryResetRemoteDatabase();
|
|
150
156
|
await this.replication.markLocked();
|
|
151
157
|
await delay(500);
|
|
@@ -170,9 +176,6 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
170
176
|
await assertDatabasePathIdentity(this.database.localDatabase.localDatabase, settings);
|
|
171
177
|
}
|
|
172
178
|
if (getPathNormalizationVersion(settings) === 0) return;
|
|
173
|
-
if (settings.remoteType !== REMOTE_COUCHDB) {
|
|
174
|
-
throw new Error("NFC path identity is supported only with CouchDB. No database has been reset.");
|
|
175
|
-
}
|
|
176
179
|
const paths = await this.storageAccess.getFileNames();
|
|
177
180
|
const selected = [];
|
|
178
181
|
const databaseReady = this.database.isDatabaseReady();
|
|
@@ -191,6 +194,19 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
191
194
|
}
|
|
192
195
|
async checkRemotePathIdentityBeforeFetch() {
|
|
193
196
|
const settings = this.setting.currentSettings();
|
|
197
|
+
if (settings.remoteType === REMOTE_MINIO) {
|
|
198
|
+
const trial = await this.replicator.getNewReplicator(settings);
|
|
199
|
+
if (!trial) throw new Error("Could not create a Journal replicator for the Fetch identity check.");
|
|
200
|
+
try {
|
|
201
|
+
if (!canCheckJournalPathIdentity(trial)) {
|
|
202
|
+
throw new Error("The Journal replicator does not support the Fetch identity check.");
|
|
203
|
+
}
|
|
204
|
+
await trial.assertRemotePathIdentity(settings);
|
|
205
|
+
} finally {
|
|
206
|
+
await trial.closeReplication();
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
194
210
|
if (settings.remoteType !== REMOTE_COUCHDB) return;
|
|
195
211
|
const remote = settings.couchDB_URI.replace(/\/+$/, "") + (settings.couchDB_DBNAME === "" ? "" : "/" + settings.couchDB_DBNAME);
|
|
196
212
|
const connection = await this.remote.connect(
|
|
@@ -206,7 +222,8 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
206
222
|
settings.useRequestAPI,
|
|
207
223
|
async () => {
|
|
208
224
|
throw new Error("A version document does not require decryption.");
|
|
209
|
-
}
|
|
225
|
+
},
|
|
226
|
+
{ useCouchDBCacheControlHeadersOnly: settings.useCouchDBCacheControlHeadersOnly === true }
|
|
210
227
|
);
|
|
211
228
|
if (typeof connection === "string") throw new Error(connection);
|
|
212
229
|
try {
|
|
@@ -262,6 +279,18 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
262
279
|
scheduleFetch(prepareBeforeRestart) {
|
|
263
280
|
return this.scheduleInitialisation(FlagFilesHumanReadable.FETCH_ALL, prepareBeforeRestart);
|
|
264
281
|
}
|
|
282
|
+
async markRemoteLockedForRebuild() {
|
|
283
|
+
const settings = this.setting.currentSettings();
|
|
284
|
+
if (settings.remoteType !== REMOTE_MINIO) {
|
|
285
|
+
await this.replication.markLocked();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const replicator = this.replicator.getActiveReplicator();
|
|
289
|
+
if (!replicator || !canLockJournalForRebuild(replicator)) {
|
|
290
|
+
throw new Error("The active Journal replicator does not support safe reconstruction.");
|
|
291
|
+
}
|
|
292
|
+
await replicator.markRemoteLockedForRebuild(settings);
|
|
293
|
+
}
|
|
265
294
|
async _tryResetRemoteDatabase() {
|
|
266
295
|
const currentReplicator = this.replicator.getActiveReplicator();
|
|
267
296
|
const settings = this.setting.currentSettings();
|
|
@@ -12,13 +12,13 @@ export declare class FileProcessingService<T extends ServiceContext = ServiceCon
|
|
|
12
12
|
* Dispatch a legacy optional notification, without confirming persistence.
|
|
13
13
|
* i.e., hidden files synchronisation or customisation sync.
|
|
14
14
|
*/
|
|
15
|
-
readonly processOptionalFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(path: import("../../
|
|
15
|
+
readonly processOptionalFileEvent: import("../lib/HandlerUtils.js").BooleanMultipleHandlerFunction<(path: import("../../storage.js").FilePath) => Promise<boolean>>;
|
|
16
16
|
/**
|
|
17
17
|
* Complete optional file operations through the first applicable handler.
|
|
18
18
|
* Hosts migrate all optional providers together. Once any typed provider is
|
|
19
19
|
* registered, dispatch uses this contract exclusively.
|
|
20
20
|
*/
|
|
21
|
-
readonly processOptionalFileEventWithResult?: import("../lib/HandlerUtils.js").FirstHandledFunction<(path: import("../../
|
|
21
|
+
readonly processOptionalFileEventWithResult?: import("../lib/HandlerUtils.js").FirstHandledFunction<(path: import("../../storage.js").FilePath, options?: import("./IService.js").OptionalFileEventOptions) => Promise<import("../lib/HandlerUtils.js").HandlerOutcome<void, string>>>;
|
|
22
22
|
/**
|
|
23
23
|
* Commit any pending file events that have been queued for processing.
|
|
24
24
|
*/
|
|
@@ -8,6 +8,8 @@ export interface RemoteConnectionOpenOptions {
|
|
|
8
8
|
readonly encryptionAlgorithm?: E2EEAlgorithm;
|
|
9
9
|
/** Apply HKDF Metadata encryption to obfuscated internal entries on writes. */
|
|
10
10
|
readonly encryptInternalMetadata?: boolean;
|
|
11
|
+
/** Use HTTP headers alone for cache control; an absent value is false. */
|
|
12
|
+
readonly useCouchDBCacheControlHeadersOnly?: boolean;
|
|
11
13
|
/**
|
|
12
14
|
* Cancels abort-capable requests made through the connection when the
|
|
13
15
|
* owning operation is cancelled.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type CouchDBCredentials, type EntryDoc } from "../../common/types.js";
|
|
2
2
|
import type { IRemoteService } from "./IService.js";
|
|
3
3
|
import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
|
|
4
4
|
import { type LOG_LEVEL } from "../../common/types.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { E2EEAlgorithms } from "../../common/types.js";
|
|
1
2
|
import { ServiceBase } from "./ServiceBase.js";
|
|
2
3
|
import {
|
|
3
4
|
LOG_LEVEL_DEBUG,
|
|
@@ -12,6 +13,7 @@ import { AuthorizationHeaderGenerator } from "../../replication/httplib.js";
|
|
|
12
13
|
import { UnresolvedErrorManager } from "./UnresolvedErrorManager.js";
|
|
13
14
|
import { createInstanceLogFunction, MARK_LOG_NETWORK_ERROR } from "../lib/logUtils.js";
|
|
14
15
|
import { runWithTrackedPhysicalRequest } from "../lib/remoteActivity.js";
|
|
16
|
+
import { createCouchDBHttpCache } from "../lib/couchdbHttpCache.js";
|
|
15
17
|
import { LiveSyncError } from "../../common/LSError.js";
|
|
16
18
|
function createRemoteConnectionScope(ownerSignal) {
|
|
17
19
|
const controller = new AbortController();
|
|
@@ -188,6 +190,12 @@ class RemoteService extends ServiceBase {
|
|
|
188
190
|
return "Network is offline";
|
|
189
191
|
}
|
|
190
192
|
const connectionScope = createRemoteConnectionScope(connectionOptions.signal);
|
|
193
|
+
const encryptionAlgorithm = connectionOptions.encryptionAlgorithm ?? this._settingService.currentSettings().E2EEAlgorithm;
|
|
194
|
+
const prepareCachedRequest = createCouchDBHttpCache(
|
|
195
|
+
uri,
|
|
196
|
+
typeof passphrase === "string" && passphrase !== "false" && encryptionAlgorithm !== E2EEAlgorithms.ForceV1 ? getPBKDF2Salt : void 0,
|
|
197
|
+
connectionOptions.useCouchDBCacheControlHeadersOnly === true
|
|
198
|
+
);
|
|
191
199
|
const conf = {
|
|
192
200
|
adapter: "http",
|
|
193
201
|
auth: "username" in auth ? auth : void 0,
|
|
@@ -222,11 +230,12 @@ class RemoteService extends ServiceBase {
|
|
|
222
230
|
if (!("username" in auth)) {
|
|
223
231
|
headers.append("authorization", authHeader);
|
|
224
232
|
}
|
|
233
|
+
const cachedRequest = await prepareCachedRequest(requestSrc, opts);
|
|
225
234
|
try {
|
|
226
235
|
const signal = connectionScope.combine(opts?.signal);
|
|
227
236
|
const response = await this.performFetch(
|
|
228
|
-
|
|
229
|
-
{ ...opts, headers, signal, cache:
|
|
237
|
+
cachedRequest.request,
|
|
238
|
+
{ ...opts, headers, signal, cache: cachedRequest.cache },
|
|
230
239
|
useRequestAPI ? FetchMethod.native : FetchMethod.webCompat
|
|
231
240
|
);
|
|
232
241
|
return response;
|
|
@@ -243,8 +252,8 @@ class RemoteService extends ServiceBase {
|
|
|
243
252
|
"Failed to fetch by native fetch API. Trying to fetch by API to get more information."
|
|
244
253
|
);
|
|
245
254
|
const resp2 = await this.performFetch(
|
|
246
|
-
|
|
247
|
-
{ ...opts, headers, cache:
|
|
255
|
+
cachedRequest.request,
|
|
256
|
+
{ ...opts, headers, cache: cachedRequest.cache },
|
|
248
257
|
FetchMethod.native
|
|
249
258
|
);
|
|
250
259
|
if (resp2.status / 100 == 2) {
|
|
@@ -279,7 +288,6 @@ class RemoteService extends ServiceBase {
|
|
|
279
288
|
}
|
|
280
289
|
}
|
|
281
290
|
};
|
|
282
|
-
const encryptionAlgorithm = connectionOptions.encryptionAlgorithm ?? this._settingService.currentSettings().E2EEAlgorithm;
|
|
283
291
|
let db;
|
|
284
292
|
try {
|
|
285
293
|
db = new this._pouchDB(uri, conf);
|
|
@@ -4,6 +4,6 @@ export declare abstract class InjectableVaultService<T extends ServiceContext> e
|
|
|
4
4
|
}
|
|
5
5
|
export declare class InjectableVaultServiceCompat<T extends ServiceContext> extends InjectableVaultService<T> {
|
|
6
6
|
isStorageInsensitive: import("../../lib/HandlerUtils.js").HandlerFunction<() => boolean, unknown>;
|
|
7
|
-
getActiveFilePath: import("../../lib/HandlerUtils.js").HandlerFunction<() => import("../../../
|
|
7
|
+
getActiveFilePath: import("../../lib/HandlerUtils.js").HandlerFunction<() => import("../../../storage.js").FilePath | undefined, unknown>;
|
|
8
8
|
isValidPath(path: string): boolean;
|
|
9
9
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Keep encrypted HTTP responses within the Security Seed which produced them. */
|
|
2
|
+
export declare function createCouchDBHttpCache(uri: string, getPBKDF2Salt?: () => Promise<Uint8Array<ArrayBuffer>>, headersOnly?: boolean): (request: string | Request, options?: RequestInit) => Promise<{
|
|
3
|
+
request: string | Request;
|
|
4
|
+
cache: RequestCache;
|
|
5
|
+
}>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { uint8ArrayToHexString } from "../../string_and_binary/convert.js";
|
|
2
|
+
const processEpoch = Math.floor(Date.now() / 1e3);
|
|
3
|
+
function createCouchDBHttpCache(uri, getPBKDF2Salt, headersOnly = false) {
|
|
4
|
+
const databasePath = new URL(uri).pathname.replace(/\/$/, "");
|
|
5
|
+
let previousSalt;
|
|
6
|
+
let namespace;
|
|
7
|
+
const getNamespace = async () => {
|
|
8
|
+
const salt = await getPBKDF2Salt();
|
|
9
|
+
if (!previousSalt || previousSalt.length !== salt.length || previousSalt.some((byte, i) => byte !== salt[i])) {
|
|
10
|
+
previousSalt = salt.slice();
|
|
11
|
+
const epoch = new TextEncoder().encode(`${processEpoch}:`);
|
|
12
|
+
const input = new Uint8Array(epoch.length + previousSalt.length);
|
|
13
|
+
input.set(epoch);
|
|
14
|
+
input.set(previousSalt, epoch.length);
|
|
15
|
+
namespace = crypto.subtle.digest("SHA-256", input).then((digest) => uint8ArrayToHexString(new Uint8Array(digest)));
|
|
16
|
+
}
|
|
17
|
+
return namespace;
|
|
18
|
+
};
|
|
19
|
+
return async (request, options) => {
|
|
20
|
+
const method = (options?.method ?? (typeof request === "string" ? "GET" : request.method)).toUpperCase();
|
|
21
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
22
|
+
return { request, cache: options?.cache };
|
|
23
|
+
}
|
|
24
|
+
if (headersOnly) return { request, cache: "no-store" };
|
|
25
|
+
const url = new URL(typeof request === "string" ? request : request.url);
|
|
26
|
+
const relativePath = url.pathname.startsWith(`${databasePath}/`) ? decodeURIComponent(url.pathname.slice(databasePath.length + 1)) : "";
|
|
27
|
+
if (getPBKDF2Salt && relativePath && !relativePath.startsWith("_local/")) {
|
|
28
|
+
url.searchParams.set("_livesync_epoch", await getNamespace());
|
|
29
|
+
request = typeof request === "string" ? url.href : new Request(url.href, request);
|
|
30
|
+
}
|
|
31
|
+
return { request, cache: "no-cache" };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
createCouchDBHttpCache
|
|
36
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Host-neutral path identity, namespace, and storage resolution contracts. */
|
|
2
|
+
export type { FilePath, DocumentPath, StoragePath, PathComparisonKey, PathComparisonSettings, DocumentPathParts, DirectoryEntry, ReadDirectory, ResolvedRename, StorageEntry, StoragePathResolver, StoragePathResolverOptions, } from "./platform/pathTypes.js";
|
|
3
|
+
export { parseFilePath, parseDocumentPath, splitDocumentPath, joinDocumentPath, getStoragePathComparisonKey, getDocumentPathComparisonKey, } from "./platform/pathComparison.js";
|
|
4
|
+
export { createStoragePathResolver, AmbiguousNormalisedPathError } from "./platform/storagePathResolution.js";
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFilePath,
|
|
3
|
+
parseDocumentPath,
|
|
4
|
+
splitDocumentPath,
|
|
5
|
+
joinDocumentPath,
|
|
6
|
+
getStoragePathComparisonKey,
|
|
7
|
+
getDocumentPathComparisonKey
|
|
8
|
+
} from "./platform/pathComparison.js";
|
|
9
|
+
import { createStoragePathResolver, AmbiguousNormalisedPathError } from "./platform/storagePathResolution.js";
|
|
10
|
+
export {
|
|
11
|
+
AmbiguousNormalisedPathError,
|
|
12
|
+
createStoragePathResolver,
|
|
13
|
+
getDocumentPathComparisonKey,
|
|
14
|
+
getStoragePathComparisonKey,
|
|
15
|
+
joinDocumentPath,
|
|
16
|
+
parseDocumentPath,
|
|
17
|
+
parseFilePath,
|
|
18
|
+
splitDocumentPath
|
|
19
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { splitPathNamespace } from "../common/pathNamespace.js";
|
|
1
2
|
import { Minimatch } from "minimatch";
|
|
2
3
|
import { getWebCrypto } from "../mods.js";
|
|
3
4
|
import {
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
import { memorizeFuncWithLRUCache } from "../common/utils.js";
|
|
14
15
|
import { uint8ArrayToHexString, writeString } from "./convert.js";
|
|
15
16
|
import { unique } from "octagonal-wheels/collection.js";
|
|
16
|
-
import { CHeader
|
|
17
|
+
import { CHeader } from "../common/models/fileaccess.const.js";
|
|
17
18
|
import { computeKeyedId } from "../common/idDerivation.js";
|
|
18
19
|
function isValidFilenameInWidows(filename) {
|
|
19
20
|
const regex = /[\u0000-\u001f]|[\\":?<>|*#]/g;
|
|
@@ -45,12 +46,8 @@ function addPrefix(path, prefix) {
|
|
|
45
46
|
return `${prefix ?? ""}${path}`;
|
|
46
47
|
}
|
|
47
48
|
function expandFilePathPrefix(path) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return [prefix, path.substring(prefix.length)];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return ["", path];
|
|
49
|
+
const [prefix, body] = splitPathNamespace(path);
|
|
50
|
+
return [prefix, body];
|
|
54
51
|
}
|
|
55
52
|
function expandDocumentIDPrefix(id) {
|
|
56
53
|
for (const prefix of [CHeader, PREFIX_OBFUSCATED]) {
|