@narumitw/pi-sync 0.50.3 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/chunks/{chunk-6LP3MCR2.ts → chunk-2JZFHNLM.ts} +3 -5
- package/dist/chunks/chunk-2JZFHNLM.ts.map +7 -0
- package/dist/chunks/{chunk-LCRI3ABW.ts → chunk-2RZSQWBH.ts} +8 -19
- package/dist/chunks/chunk-2RZSQWBH.ts.map +7 -0
- package/dist/chunks/{chunk-IJ6FJFA5.ts → chunk-4AIHXRKE.ts} +11 -17
- package/dist/chunks/chunk-4AIHXRKE.ts.map +7 -0
- package/dist/chunks/{chunk-C7XY5NF5.ts → chunk-4MPIH46N.ts} +29 -51
- package/dist/chunks/chunk-4MPIH46N.ts.map +7 -0
- package/dist/chunks/{chunk-5B3FXBLE.ts → chunk-6546TS54.ts} +16 -20
- package/dist/chunks/chunk-6546TS54.ts.map +7 -0
- package/dist/chunks/{chunk-Q2TUM5X6.ts → chunk-6NT3YFGY.ts} +2 -2
- package/dist/chunks/chunk-6NT3YFGY.ts.map +7 -0
- package/dist/chunks/{chunk-2B2ELUF5.ts → chunk-7D5SF7P3.ts} +1 -1
- package/dist/chunks/chunk-7D5SF7P3.ts.map +7 -0
- package/dist/chunks/{chunk-YDWUZVPN.ts → chunk-7KHKWL3Q.ts} +28 -47
- package/dist/chunks/chunk-7KHKWL3Q.ts.map +7 -0
- package/dist/chunks/{chunk-ZI5MXMEX.ts → chunk-BVVP33U5.ts} +2 -4
- package/dist/chunks/chunk-BVVP33U5.ts.map +7 -0
- package/dist/chunks/{chunk-SZWBGPID.ts → chunk-DM4X4FIA.ts} +8 -13
- package/dist/chunks/chunk-DM4X4FIA.ts.map +7 -0
- package/dist/chunks/{chunk-SL64RLXR.ts → chunk-DZUSTGQV.ts} +8 -16
- package/dist/chunks/chunk-DZUSTGQV.ts.map +7 -0
- package/dist/chunks/{chunk-YHEMENKV.ts → chunk-E42GXD6D.ts} +13 -19
- package/dist/chunks/chunk-E42GXD6D.ts.map +7 -0
- package/dist/chunks/{chunk-Z4DQF6L3.ts → chunk-ECYCNZOE.ts} +1 -1
- package/dist/chunks/chunk-ECYCNZOE.ts.map +7 -0
- package/dist/chunks/{chunk-OEX4ABY6.ts → chunk-EEJPYTWE.ts} +11 -21
- package/dist/chunks/chunk-EEJPYTWE.ts.map +7 -0
- package/dist/chunks/{chunk-CPEHGXJV.ts → chunk-G4RUXRWZ.ts} +3 -5
- package/dist/chunks/chunk-G4RUXRWZ.ts.map +7 -0
- package/dist/chunks/{chunk-T2B5IU74.ts → chunk-HGJH56KR.ts} +4 -4
- package/dist/chunks/chunk-HGJH56KR.ts.map +7 -0
- package/dist/chunks/{chunk-JAAKLL6M.ts → chunk-IVXEDPAD.ts} +10 -34
- package/dist/chunks/chunk-IVXEDPAD.ts.map +7 -0
- package/dist/chunks/chunk-K3RIEL6J.ts +20 -0
- package/dist/chunks/chunk-K3RIEL6J.ts.map +7 -0
- package/dist/chunks/{chunk-LTBWGSBV.ts → chunk-MSSW6F3R.ts} +1 -1
- package/dist/chunks/chunk-MSSW6F3R.ts.map +7 -0
- package/dist/chunks/{chunk-T3HH3UEJ.ts → chunk-OVDTF5CH.ts} +4 -10
- package/dist/chunks/chunk-OVDTF5CH.ts.map +7 -0
- package/dist/chunks/{chunk-NJFUIVYL.ts → chunk-PWMWOJP4.ts} +1 -1
- package/dist/chunks/chunk-PWMWOJP4.ts.map +7 -0
- package/dist/chunks/{chunk-HUDQATCL.ts → chunk-Q32CQWPM.ts} +6 -8
- package/dist/chunks/chunk-Q32CQWPM.ts.map +7 -0
- package/dist/chunks/{chunk-ETTRJDNP.ts → chunk-QLSN7AGX.ts} +6 -13
- package/dist/chunks/chunk-QLSN7AGX.ts.map +7 -0
- package/dist/chunks/{chunk-THMF4BVL.ts → chunk-SWUXA3MH.ts} +1 -1
- package/dist/chunks/chunk-SWUXA3MH.ts.map +7 -0
- package/dist/chunks/{chunk-TEN2ZBJV.ts → chunk-T2SJD3N7.ts} +2 -4
- package/dist/chunks/chunk-T2SJD3N7.ts.map +7 -0
- package/dist/chunks/{chunk-AUSDLD6F.ts → chunk-T56RNRZJ.ts} +10 -26
- package/dist/chunks/chunk-T56RNRZJ.ts.map +7 -0
- package/dist/chunks/{chunk-DHVFS4QK.ts → chunk-UA7RXOIT.ts} +37 -90
- package/dist/chunks/chunk-UA7RXOIT.ts.map +7 -0
- package/dist/chunks/{chunk-D5X27LCA.ts → chunk-UETBKTDI.ts} +1 -1
- package/dist/chunks/chunk-UETBKTDI.ts.map +7 -0
- package/dist/chunks/{chunk-CHH26D4F.ts → chunk-YFADF5ZQ.ts} +7 -6
- package/dist/chunks/chunk-YFADF5ZQ.ts.map +7 -0
- package/dist/chunks/{chunk-CCSN3F47.ts → chunk-YPXGU5OR.ts} +4 -4
- package/dist/chunks/chunk-YPXGU5OR.ts.map +7 -0
- package/dist/chunks/{file-selection-FTTKAOYX.ts → file-selection-RAVHRYFG.ts} +14 -26
- package/dist/chunks/file-selection-RAVHRYFG.ts.map +7 -0
- package/dist/chunks/{git-backend-GMVS4PD4.ts → git-backend-IAANDL3K.ts} +41 -104
- package/dist/chunks/git-backend-IAANDL3K.ts.map +7 -0
- package/dist/chunks/manager-result-dispatcher-OTUMQM26.ts +23 -0
- package/dist/chunks/{manager-ui-IPAOAWZA.ts → manager-ui-763NEKJY.ts} +124 -251
- package/dist/chunks/manager-ui-763NEKJY.ts.map +7 -0
- package/dist/chunks/remote-selection-ui-TZTJF32Y.ts +22 -0
- package/dist/chunks/{s3-backend-66O4FGBH.ts → s3-backend-XGXUOPUN.ts} +23 -54
- package/dist/chunks/s3-backend-XGXUOPUN.ts.map +7 -0
- package/dist/chunks/{setup-switch-IS2PEZ3J.ts → setup-switch-5G2VNJHL.ts} +9 -9
- package/dist/chunks/{setup-wizard-JUJZYNNM.ts → setup-wizard-YIEWGX32.ts} +12 -12
- package/dist/chunks/setup-wizard-YIEWGX32.ts.map +7 -0
- package/dist/chunks/{snapshot-HKEWCVK6.ts → snapshot-JL5DFSXI.ts} +5 -5
- package/dist/chunks/sync-inspection-P3ENJLNF.ts +20 -0
- package/dist/chunks/{sync-operations-LBX3GKCM.ts → sync-operations-AR76EB6C.ts} +85 -201
- package/dist/chunks/sync-operations-AR76EB6C.ts.map +7 -0
- package/dist/chunks/{sync-state-Q2FTXTW6.ts → sync-state-VHJUZ4YG.ts} +6 -6
- package/dist/chunks/{webdav-backend-ECIKD7SO.ts → webdav-backend-3EGUONAT.ts} +20 -51
- package/dist/chunks/webdav-backend-3EGUONAT.ts.map +7 -0
- package/dist/index.ts +49 -74
- package/dist/index.ts.map +3 -3
- package/docs/settings.md +9 -0
- package/package.json +61 -62
- package/src/backends/backend-factory.ts +15 -15
- package/src/backends/backend-identity.ts +31 -31
- package/src/backends/backend-types.ts +42 -42
- package/src/backends/git/git-backend.ts +714 -788
- package/src/backends/git/git-config.ts +104 -111
- package/src/backends/git/git-runner.ts +230 -247
- package/src/backends/git/git-storage.ts +228 -245
- package/src/backends/s3/s3-backend.ts +408 -441
- package/src/backends/s3/s3-client.ts +288 -333
- package/src/backends/sync-backend.ts +65 -65
- package/src/backends/webdav/webdav-backend.ts +542 -589
- package/src/backends/webdav/webdav-client.ts +292 -316
- package/src/backends/webdav/webdav-config.ts +57 -68
- package/src/commands/command-execution.ts +270 -289
- package/src/commands/command-handler.ts +160 -163
- package/src/commands/command-types.ts +13 -13
- package/src/commands/command.ts +190 -196
- package/src/commands/setup-completions.ts +1 -1
- package/src/paths.ts +47 -47
- package/src/settings/config-errors.ts +4 -5
- package/src/settings/config-file.ts +473 -506
- package/src/settings/config.ts +171 -174
- package/src/settings/settings-management.ts +191 -216
- package/src/settings/settings-store.ts +63 -77
- package/src/settings/settings-types.ts +78 -75
- package/src/settings/settings-validation.ts +328 -364
- package/src/snapshot/session-paths.ts +41 -55
- package/src/snapshot/snapshot-apply.ts +197 -229
- package/src/snapshot/snapshot-codec.ts +29 -31
- package/src/snapshot/snapshot-paths.ts +4 -6
- package/src/snapshot/snapshot-transaction.ts +161 -170
- package/src/snapshot/snapshot-types.ts +24 -24
- package/src/snapshot/snapshot.ts +310 -343
- package/src/state/json-file.ts +11 -11
- package/src/state/lock.ts +226 -242
- package/src/state/lockfile-fs.ts +20 -20
- package/src/state/operation-availability.ts +36 -41
- package/src/state/state-directory.ts +171 -173
- package/src/state/state-types.ts +15 -15
- package/src/state/sync-state-store.ts +23 -23
- package/src/sync/automatic-sync.ts +75 -80
- package/src/sync/remote-snapshot.ts +52 -60
- package/src/sync/setup-switch.ts +99 -114
- package/src/sync/signals.ts +3 -5
- package/src/sync/startup-check.ts +97 -111
- package/src/sync/sync-decision.ts +76 -80
- package/src/sync/sync-error-guidance.ts +41 -48
- package/src/sync/sync-errors.ts +10 -10
- package/src/sync/sync-inspection.ts +29 -33
- package/src/sync/sync-loaders.ts +28 -36
- package/src/sync/sync-mutations.ts +510 -622
- package/src/sync/sync-operations.ts +7 -7
- package/src/sync/sync-policy.ts +296 -326
- package/src/sync/sync-queries.ts +175 -194
- package/src/sync/sync-state.ts +74 -100
- package/src/sync-extension.ts +89 -101
- package/src/sync.ts +26 -26
- package/src/types.ts +37 -37
- package/src/ui/automatic-sync-summary.ts +1 -3
- package/src/ui/cancellable-operation.ts +89 -101
- package/src/ui/file-selection.ts +250 -274
- package/src/ui/manager-attention.ts +51 -63
- package/src/ui/manager-recovery.ts +48 -57
- package/src/ui/manager-result-dispatcher.ts +62 -76
- package/src/ui/manager-state.ts +200 -232
- package/src/ui/manager-ui.ts +264 -362
- package/src/ui/remote-selection-ui.ts +547 -588
- package/src/ui/secret-input.ts +197 -205
- package/src/ui/settings-ui.ts +175 -172
- package/src/ui/setup/git-ui.ts +232 -261
- package/src/ui/setup/s3-credentials-ui.ts +58 -65
- package/src/ui/setup/s3-ui.ts +171 -195
- package/src/ui/setup/setup-actions.ts +91 -101
- package/src/ui/setup/setup-location-ui.ts +121 -137
- package/src/ui/setup/setup-name-ui.ts +27 -27
- package/src/ui/setup/setup-prompts.ts +65 -65
- package/src/ui/setup/setup-review.ts +58 -59
- package/src/ui/setup/setup-switcher.ts +156 -160
- package/src/ui/setup/setup-wizard.ts +28 -28
- package/src/ui/setup/text-input.ts +54 -58
- package/src/ui/setup/webdav-ui.ts +246 -293
- package/src/ui/storage-connections-ui.ts +317 -369
- package/src/ui/sync-attention.ts +148 -164
- package/src/ui/sync-format.ts +110 -121
- package/src/ui/sync-resolution-ui.ts +147 -153
- package/src/ui/sync-setups-ui.ts +218 -238
- package/src/ui/sync-status.ts +17 -0
- package/src/ui/terminal-text.ts +2 -2
- package/dist/chunks/chunk-2B2ELUF5.ts.map +0 -7
- package/dist/chunks/chunk-5B3FXBLE.ts.map +0 -7
- package/dist/chunks/chunk-6LP3MCR2.ts.map +0 -7
- package/dist/chunks/chunk-AUSDLD6F.ts.map +0 -7
- package/dist/chunks/chunk-C7XY5NF5.ts.map +0 -7
- package/dist/chunks/chunk-CCSN3F47.ts.map +0 -7
- package/dist/chunks/chunk-CHH26D4F.ts.map +0 -7
- package/dist/chunks/chunk-CPEHGXJV.ts.map +0 -7
- package/dist/chunks/chunk-D5X27LCA.ts.map +0 -7
- package/dist/chunks/chunk-DHVFS4QK.ts.map +0 -7
- package/dist/chunks/chunk-ETTRJDNP.ts.map +0 -7
- package/dist/chunks/chunk-HUDQATCL.ts.map +0 -7
- package/dist/chunks/chunk-IJ6FJFA5.ts.map +0 -7
- package/dist/chunks/chunk-JAAKLL6M.ts.map +0 -7
- package/dist/chunks/chunk-LCRI3ABW.ts.map +0 -7
- package/dist/chunks/chunk-LTBWGSBV.ts.map +0 -7
- package/dist/chunks/chunk-NJFUIVYL.ts.map +0 -7
- package/dist/chunks/chunk-OEX4ABY6.ts.map +0 -7
- package/dist/chunks/chunk-Q2TUM5X6.ts.map +0 -7
- package/dist/chunks/chunk-SL64RLXR.ts.map +0 -7
- package/dist/chunks/chunk-SZWBGPID.ts.map +0 -7
- package/dist/chunks/chunk-T2B5IU74.ts.map +0 -7
- package/dist/chunks/chunk-T3HH3UEJ.ts.map +0 -7
- package/dist/chunks/chunk-TEN2ZBJV.ts.map +0 -7
- package/dist/chunks/chunk-THMF4BVL.ts.map +0 -7
- package/dist/chunks/chunk-YDWUZVPN.ts.map +0 -7
- package/dist/chunks/chunk-YHEMENKV.ts.map +0 -7
- package/dist/chunks/chunk-Z4DQF6L3.ts.map +0 -7
- package/dist/chunks/chunk-ZI5MXMEX.ts.map +0 -7
- package/dist/chunks/file-selection-FTTKAOYX.ts.map +0 -7
- package/dist/chunks/git-backend-GMVS4PD4.ts.map +0 -7
- package/dist/chunks/manager-result-dispatcher-ASKGOSW5.ts +0 -22
- package/dist/chunks/manager-ui-IPAOAWZA.ts.map +0 -7
- package/dist/chunks/remote-selection-ui-QDNR7WEI.ts +0 -21
- package/dist/chunks/s3-backend-66O4FGBH.ts.map +0 -7
- package/dist/chunks/setup-wizard-JUJZYNNM.ts.map +0 -7
- package/dist/chunks/sync-inspection-CST2KL65.ts +0 -20
- package/dist/chunks/sync-operations-LBX3GKCM.ts.map +0 -7
- package/dist/chunks/webdav-backend-ECIKD7SO.ts.map +0 -7
- /package/dist/chunks/{manager-result-dispatcher-ASKGOSW5.ts.map → manager-result-dispatcher-OTUMQM26.ts.map} +0 -0
- /package/dist/chunks/{remote-selection-ui-QDNR7WEI.ts.map → remote-selection-ui-TZTJF32Y.ts.map} +0 -0
- /package/dist/chunks/{setup-switch-IS2PEZ3J.ts.map → setup-switch-5G2VNJHL.ts.map} +0 -0
- /package/dist/chunks/{snapshot-HKEWCVK6.ts.map → snapshot-JL5DFSXI.ts.map} +0 -0
- /package/dist/chunks/{sync-inspection-CST2KL65.ts.map → sync-inspection-P3ENJLNF.ts.map} +0 -0
- /package/dist/chunks/{sync-state-Q2FTXTW6.ts.map → sync-state-VHJUZ4YG.ts.map} +0 -0
|
@@ -1,443 +1,407 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { normalizeEndpointIdentity } from "../backends/backend-identity.js";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
normalizeGitBranch,
|
|
5
|
+
normalizeGitDirectory,
|
|
6
|
+
normalizeGitRemote,
|
|
7
|
+
normalizeGitRemoteIdentity,
|
|
8
8
|
} from "../backends/git/git-config.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
normalizeWebDavIdentityUrl,
|
|
11
|
+
normalizeWebDavPath,
|
|
12
|
+
normalizeWebDavUrl,
|
|
13
|
+
validateWebDavCredentials,
|
|
14
14
|
} from "../backends/webdav/webdav-config.js";
|
|
15
15
|
import { normalizeSyncInclude } from "../sync/sync-policy.js";
|
|
16
16
|
import { localConfigPath } from "./config-file.js";
|
|
17
17
|
import type {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
OnSwitchAction,
|
|
19
|
+
PiSyncSettingsV3,
|
|
20
|
+
StorageConnectionSettings,
|
|
21
|
+
SyncSetupSettings,
|
|
22
22
|
} from "./settings-types.js";
|
|
23
23
|
|
|
24
24
|
export const DEFAULT_ON_SWITCH: OnSwitchAction = "ask-before-pull";
|
|
25
25
|
|
|
26
26
|
export function normalizeOnSwitch(value: unknown): OnSwitchAction {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
if (value === "ask-before-pull" || value === "pull-after-switch" || value === "switch-only") {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Invalid pi-sync settings: onSwitch must be "ask-before-pull", "pull-after-switch", or "switch-only".',
|
|
32
|
+
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function validateSettingsDocument(value: Record<string, unknown>): PiSyncSettingsV3 {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
36
|
+
if (value.version !== 3) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unsupported pi-sync settings: version 3 is required. Keep the existing file for recovery, then create a new version 3 ${path.basename(localConfigPath())}; pi-sync will not migrate or overwrite old settings.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
rejectLegacyFields(
|
|
42
|
+
value,
|
|
43
|
+
[
|
|
44
|
+
"profiles",
|
|
45
|
+
"targets",
|
|
46
|
+
"activeTarget",
|
|
47
|
+
"targetSwitchAction",
|
|
48
|
+
"endpoint",
|
|
49
|
+
"bucket",
|
|
50
|
+
"region",
|
|
51
|
+
"accessKeyId",
|
|
52
|
+
"secretAccessKey",
|
|
53
|
+
"sessionToken",
|
|
54
|
+
"profile",
|
|
55
|
+
"prefix",
|
|
56
|
+
"autoSync",
|
|
57
|
+
"syncFiles",
|
|
58
|
+
"syncSessions",
|
|
59
|
+
"extraFiles",
|
|
60
|
+
],
|
|
61
|
+
"top level",
|
|
62
|
+
);
|
|
63
|
+
normalizeOnSwitch(value.onSwitch);
|
|
64
|
+
if (value.skipSecretScan !== undefined && typeof value.skipSecretScan !== "boolean") {
|
|
65
|
+
throw new Error("Invalid pi-sync settings: skipSecretScan must be boolean.");
|
|
66
|
+
}
|
|
67
|
+
if (value.showStatus !== undefined && typeof value.showStatus !== "boolean") {
|
|
68
|
+
throw new Error("Invalid pi-sync settings: showStatus must be boolean.");
|
|
69
|
+
}
|
|
70
|
+
const storageConnections = requireNamedObjectMap(
|
|
71
|
+
value.storageConnections,
|
|
72
|
+
"storageConnections",
|
|
73
|
+
"storage connection",
|
|
74
|
+
);
|
|
75
|
+
const syncSetups = requireNamedObjectMap(value.syncSetups, "syncSetups", "sync setup");
|
|
76
|
+
for (const name of Object.keys(storageConnections)) {
|
|
77
|
+
validateStorageConnection(
|
|
78
|
+
name,
|
|
79
|
+
requireOwnObject(storageConnections, name, "storage connection") as Record<string, unknown>,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
for (const name of Object.keys(syncSetups)) {
|
|
83
|
+
validateSyncSetup(
|
|
84
|
+
name,
|
|
85
|
+
requireOwnObject(syncSetups, name, "sync setup") as Record<string, unknown>,
|
|
86
|
+
storageConnections,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const names = Object.keys(syncSetups);
|
|
90
|
+
const activeSyncSetup = optionalCanonicalReference(value.activeSyncSetup, "activeSyncSetup");
|
|
91
|
+
if (names.length === 0) {
|
|
92
|
+
if (activeSyncSetup !== undefined) {
|
|
93
|
+
throw new Error("Invalid pi-sync settings: empty syncSetups cannot have activeSyncSetup.");
|
|
94
|
+
}
|
|
95
|
+
} else if (!activeSyncSetup || !Object.hasOwn(syncSetups, activeSyncSetup)) {
|
|
96
|
+
throw new Error("Invalid pi-sync settings: activeSyncSetup must reference an existing own-property sync setup.");
|
|
97
|
+
}
|
|
98
|
+
validateUniqueRemoteSyncSetups(syncSetups, storageConnections);
|
|
99
|
+
return value as PiSyncSettingsV3;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
function validateStorageConnection(name: string, value: Record<string, unknown>) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
requiredString(value.region, `S3 region for “${name}”`);
|
|
149
|
-
requiredString(credentials.accessKeyId, `S3 access key id for “${name}”`);
|
|
150
|
-
requiredSecret(credentials.secretAccessKey, `S3 secret access key for “${name}”`);
|
|
151
|
-
optionalString(credentials.sessionToken, `S3 session token for “${name}”`);
|
|
152
|
-
if (["username", "password"].some((field) => Object.hasOwn(credentials, field))) {
|
|
153
|
-
throw new Error(`Invalid pi-sync settings: S3 credentials for “${name}” mix fields.`);
|
|
154
|
-
}
|
|
103
|
+
rejectLegacyFields(
|
|
104
|
+
value,
|
|
105
|
+
["kind", "accessKeyId", "secretAccessKey", "sessionToken", "username", "password"],
|
|
106
|
+
`storage connection “${name}”`,
|
|
107
|
+
);
|
|
108
|
+
const type = requiredString(value.type, `storage connection “${name}” type`);
|
|
109
|
+
if (type !== "s3" && type !== "git" && type !== "webdav") {
|
|
110
|
+
throw new Error(`Invalid pi-sync settings: storage connection “${name}” has unsupported type.`);
|
|
111
|
+
}
|
|
112
|
+
const known = ["endpoint", "region", "remote", "url", "credentials"];
|
|
113
|
+
const allowed =
|
|
114
|
+
type === "s3"
|
|
115
|
+
? new Set(["endpoint", "region", "credentials"])
|
|
116
|
+
: type === "git"
|
|
117
|
+
? new Set(["remote"])
|
|
118
|
+
: new Set(["url", "credentials"]);
|
|
119
|
+
if (known.some((field) => Object.hasOwn(value, field) && !allowed.has(field))) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Invalid pi-sync settings: ${type.toUpperCase()} storage connection “${name}” mixes backend fields.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (type === "git") {
|
|
125
|
+
if (!normalizeGitRemote(requiredString(value.remote, `Git remote for “${name}”`))) {
|
|
126
|
+
throw new Error(`Invalid pi-sync settings: Git remote for “${name}” is required.`);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const credentials = requireRecord(value.credentials, `credentials for storage connection “${name}”`);
|
|
131
|
+
if (type === "webdav") {
|
|
132
|
+
normalizeWebDavUrl(requiredString(value.url, `WebDAV URL for “${name}”`));
|
|
133
|
+
const username = requiredString(credentials.username, `WebDAV username for “${name}”`);
|
|
134
|
+
const password = requiredSecret(credentials.password, `WebDAV password for “${name}”`);
|
|
135
|
+
if (["accessKeyId", "secretAccessKey", "sessionToken"].some((field) => Object.hasOwn(credentials, field))) {
|
|
136
|
+
throw new Error(`Invalid pi-sync settings: WebDAV credentials for “${name}” mix fields.`);
|
|
137
|
+
}
|
|
138
|
+
validateWebDavCredentials(username, password);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
normalizeS3Endpoint(requiredString(value.endpoint, `S3 endpoint for “${name}”`));
|
|
142
|
+
requiredString(value.region, `S3 region for “${name}”`);
|
|
143
|
+
requiredString(credentials.accessKeyId, `S3 access key id for “${name}”`);
|
|
144
|
+
requiredSecret(credentials.secretAccessKey, `S3 secret access key for “${name}”`);
|
|
145
|
+
optionalString(credentials.sessionToken, `S3 session token for “${name}”`);
|
|
146
|
+
if (["username", "password"].some((field) => Object.hasOwn(credentials, field))) {
|
|
147
|
+
throw new Error(`Invalid pi-sync settings: S3 credentials for “${name}” mix fields.`);
|
|
148
|
+
}
|
|
155
149
|
}
|
|
156
150
|
|
|
157
|
-
function validateSyncSetup(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
if (!Object.hasOwn(sync, "include")) {
|
|
216
|
-
throw new Error(`Invalid pi-sync settings: sync setup “${name}” is missing sync.include.`);
|
|
217
|
-
}
|
|
218
|
-
normalizeSyncInclude(sync.include);
|
|
219
|
-
if (typeof sync.automatic !== "boolean") {
|
|
220
|
-
throw new Error(
|
|
221
|
-
`Invalid pi-sync settings: sync setup “${name}” sync.automatic must be boolean.`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
151
|
+
function validateSyncSetup(name: string, value: Record<string, unknown>, connections: Record<string, unknown>) {
|
|
152
|
+
rejectLegacyFields(
|
|
153
|
+
value,
|
|
154
|
+
[
|
|
155
|
+
"profile",
|
|
156
|
+
"bucket",
|
|
157
|
+
"branch",
|
|
158
|
+
"path",
|
|
159
|
+
"prefix",
|
|
160
|
+
"directory",
|
|
161
|
+
"namespace",
|
|
162
|
+
"autoSync",
|
|
163
|
+
"syncFiles",
|
|
164
|
+
"syncSessions",
|
|
165
|
+
"extraFiles",
|
|
166
|
+
],
|
|
167
|
+
`sync setup “${name}”`,
|
|
168
|
+
);
|
|
169
|
+
const storage = requireRecord(value.storage, `storage for sync setup “${name}”`);
|
|
170
|
+
const sync = requireRecord(value.sync, `sync policy for sync setup “${name}”`);
|
|
171
|
+
rejectLegacyFields(storage, ["profile", "prefix", "directory", "namespace"], `storage for sync setup “${name}”`);
|
|
172
|
+
rejectLegacyFields(
|
|
173
|
+
sync,
|
|
174
|
+
["autoSync", "syncFiles", "syncSessions", "extraFiles"],
|
|
175
|
+
`sync policy for sync setup “${name}”`,
|
|
176
|
+
);
|
|
177
|
+
const connectionName = requiredCanonicalReference(
|
|
178
|
+
storage.connection,
|
|
179
|
+
`storage connection reference for sync setup “${name}”`,
|
|
180
|
+
);
|
|
181
|
+
validateConfigName(connectionName, "storage connection reference");
|
|
182
|
+
const connection = ownObject<Record<string, unknown>>(connections, connectionName);
|
|
183
|
+
if (!connection) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Invalid pi-sync settings: sync setup “${name}” references missing storage connection “${connectionName}”.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const type = connection.type;
|
|
189
|
+
normalizeStoragePath(requiredString(storage.path, `storage path for sync setup “${name}”`));
|
|
190
|
+
if (type === "s3") {
|
|
191
|
+
normalizeS3Bucket(requiredString(storage.bucket, `S3 bucket for sync setup “${name}”`));
|
|
192
|
+
if (Object.hasOwn(storage, "branch")) mixedSetupError("S3", name);
|
|
193
|
+
} else if (type === "git") {
|
|
194
|
+
normalizeGitBranch(requiredString(storage.branch, `Git branch for sync setup “${name}”`));
|
|
195
|
+
if (Object.hasOwn(storage, "bucket")) mixedSetupError("Git", name);
|
|
196
|
+
} else if (type === "webdav") {
|
|
197
|
+
if (Object.hasOwn(storage, "bucket") || Object.hasOwn(storage, "branch")) {
|
|
198
|
+
mixedSetupError("WebDAV", name);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!Object.hasOwn(sync, "include")) {
|
|
202
|
+
throw new Error(`Invalid pi-sync settings: sync setup “${name}” is missing sync.include.`);
|
|
203
|
+
}
|
|
204
|
+
normalizeSyncInclude(sync.include);
|
|
205
|
+
if (typeof sync.automatic !== "boolean") {
|
|
206
|
+
throw new Error(`Invalid pi-sync settings: sync setup “${name}” sync.automatic must be boolean.`);
|
|
207
|
+
}
|
|
224
208
|
}
|
|
225
209
|
|
|
226
210
|
function mixedSetupError(type: string, name: string): never {
|
|
227
|
-
|
|
211
|
+
throw new Error(`Invalid pi-sync settings: ${type} sync setup “${name}” mixes backend fields.`);
|
|
228
212
|
}
|
|
229
213
|
|
|
230
|
-
export function validateUniqueRemoteSyncSetups(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
identities.set(identity, name);
|
|
250
|
-
}
|
|
214
|
+
export function validateUniqueRemoteSyncSetups(setups: Record<string, unknown>, connections: Record<string, unknown>) {
|
|
215
|
+
const identities = new Map<string, string>();
|
|
216
|
+
for (const name of Object.keys(setups)) {
|
|
217
|
+
const setup = requireOwnObject(setups, name, "sync setup") as unknown as SyncSetupSettings;
|
|
218
|
+
const connection = requireOwnObject(
|
|
219
|
+
connections,
|
|
220
|
+
setup.storage.connection,
|
|
221
|
+
"storage connection",
|
|
222
|
+
) as unknown as StorageConnectionSettings;
|
|
223
|
+
const identity = effectiveSyncSetupRemoteIdentity(setup, connection);
|
|
224
|
+
const existing = identities.get(identity);
|
|
225
|
+
if (existing) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Invalid pi-sync settings: sync setups “${existing}” and “${name}” use the same normalized remote location.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
identities.set(identity, name);
|
|
231
|
+
}
|
|
251
232
|
}
|
|
252
233
|
|
|
253
|
-
export function effectiveSyncSetupRemoteIdentity(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
normalizeS3Bucket(setup.storage.bucket),
|
|
278
|
-
storagePath,
|
|
279
|
-
]);
|
|
234
|
+
export function effectiveSyncSetupRemoteIdentity(setup: SyncSetupSettings, connection: StorageConnectionSettings) {
|
|
235
|
+
const storagePath = normalizeStoragePath(setup.storage.path);
|
|
236
|
+
if (connection.type === "git") {
|
|
237
|
+
return JSON.stringify([
|
|
238
|
+
"git",
|
|
239
|
+
normalizeGitRemoteIdentity(connection.remote),
|
|
240
|
+
normalizeGitBranch(setup.storage.branch),
|
|
241
|
+
normalizeGitDirectory(storagePath),
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
if (connection.type === "webdav") {
|
|
245
|
+
return JSON.stringify([
|
|
246
|
+
"webdav",
|
|
247
|
+
normalizeWebDavIdentityUrl(connection.url),
|
|
248
|
+
connection.credentials.username.trim(),
|
|
249
|
+
normalizeWebDavPath(storagePath),
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
return JSON.stringify([
|
|
253
|
+
"s3",
|
|
254
|
+
normalizeEndpointIdentity(connection.endpoint),
|
|
255
|
+
normalizeS3Bucket(setup.storage.bucket),
|
|
256
|
+
storagePath,
|
|
257
|
+
]);
|
|
280
258
|
}
|
|
281
259
|
|
|
282
|
-
function rejectLegacyFields(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const field = fields.find((candidate) => Object.hasOwn(value, candidate));
|
|
288
|
-
if (field) {
|
|
289
|
-
throw new Error(
|
|
290
|
-
`Invalid pi-sync settings: ${context} contains unsupported version 1/2 field “${field}”.`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
260
|
+
function rejectLegacyFields(value: Record<string, unknown>, fields: readonly string[], context: string) {
|
|
261
|
+
const field = fields.find((candidate) => Object.hasOwn(value, candidate));
|
|
262
|
+
if (field) {
|
|
263
|
+
throw new Error(`Invalid pi-sync settings: ${context} contains unsupported version 1/2 field “${field}”.`);
|
|
264
|
+
}
|
|
293
265
|
}
|
|
294
266
|
|
|
295
267
|
function requireNamedObjectMap(value: unknown, field: string, itemLabel: string) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
268
|
+
const result = requireRecord(value, field);
|
|
269
|
+
for (const name of Object.keys(result)) validateConfigName(name, itemLabel);
|
|
270
|
+
return result;
|
|
299
271
|
}
|
|
300
272
|
|
|
301
273
|
function requireOwnObject(value: Record<string, unknown>, key: string, label: string) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
274
|
+
const item = ownObject(value, key);
|
|
275
|
+
if (!item) throw new Error(`Invalid pi-sync settings: ${label} “${key}” must be an object.`);
|
|
276
|
+
return item;
|
|
305
277
|
}
|
|
306
278
|
|
|
307
|
-
export function ownObject<T extends object>(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
if (!Object.hasOwn(value, key)) return undefined;
|
|
312
|
-
const item = value[key];
|
|
313
|
-
return item && typeof item === "object" && !Array.isArray(item) ? (item as T) : undefined;
|
|
279
|
+
export function ownObject<T extends object>(value: Record<string, unknown>, key: string): T | undefined {
|
|
280
|
+
if (!Object.hasOwn(value, key)) return undefined;
|
|
281
|
+
const item = value[key];
|
|
282
|
+
return item && typeof item === "object" && !Array.isArray(item) ? (item as T) : undefined;
|
|
314
283
|
}
|
|
315
284
|
|
|
316
285
|
function requireRecord(value: unknown, field: string): Record<string, unknown> {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
286
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
287
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be an object.`);
|
|
288
|
+
}
|
|
289
|
+
return value as Record<string, unknown>;
|
|
321
290
|
}
|
|
322
291
|
|
|
323
292
|
export function validateConfigName(value: string, field: string) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
293
|
+
if (
|
|
294
|
+
!value.trim() ||
|
|
295
|
+
value !== value.trim() ||
|
|
296
|
+
value.length > 100 ||
|
|
297
|
+
value === "__proto__" ||
|
|
298
|
+
value === "prototype" ||
|
|
299
|
+
value === "constructor" ||
|
|
300
|
+
hasControlCharacter(value)
|
|
301
|
+
) {
|
|
302
|
+
throw new Error(`Invalid pi-sync settings: invalid ${field} name.`);
|
|
303
|
+
}
|
|
335
304
|
}
|
|
336
305
|
|
|
337
306
|
export function requiredString(value: unknown, field: string) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
307
|
+
if (typeof value !== "string" || !value.trim() || hasControlCharacter(value)) {
|
|
308
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be a non-empty string.`);
|
|
309
|
+
}
|
|
310
|
+
return value.trim();
|
|
342
311
|
}
|
|
343
312
|
|
|
344
313
|
function requiredCanonicalReference(value: unknown, field: string) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
314
|
+
const normalized = requiredString(value, field);
|
|
315
|
+
if (value !== normalized) {
|
|
316
|
+
throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`);
|
|
317
|
+
}
|
|
318
|
+
return normalized;
|
|
350
319
|
}
|
|
351
320
|
|
|
352
321
|
function optionalCanonicalReference(value: unknown, field: string) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
322
|
+
const normalized = optionalString(value, field);
|
|
323
|
+
if (normalized !== undefined && value !== normalized) {
|
|
324
|
+
throw new Error(`Invalid pi-sync settings: ${field} must not have surrounding whitespace.`);
|
|
325
|
+
}
|
|
326
|
+
return normalized;
|
|
358
327
|
}
|
|
359
328
|
|
|
360
329
|
function requiredSecret(value: unknown, field: string) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
330
|
+
if (typeof value !== "string" || !value || hasControlCharacter(value)) {
|
|
331
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be configured.`);
|
|
332
|
+
}
|
|
333
|
+
return value;
|
|
365
334
|
}
|
|
366
335
|
|
|
367
336
|
export function optionalString(value: unknown, field: string) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
337
|
+
if (value === undefined) return undefined;
|
|
338
|
+
if (typeof value !== "string" || hasControlCharacter(value)) {
|
|
339
|
+
throw new Error(`Invalid pi-sync settings: ${field} must be a string.`);
|
|
340
|
+
}
|
|
341
|
+
return value.trim() || undefined;
|
|
373
342
|
}
|
|
374
343
|
|
|
375
344
|
export function normalizeStoragePath(value: string) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
345
|
+
if (value.trim() === "." || value.trim() === "./") return "./";
|
|
346
|
+
const normalized = value.trim().replace(/^\/+|\/+$/gu, "");
|
|
347
|
+
if (
|
|
348
|
+
!normalized ||
|
|
349
|
+
normalized.length > 1024 ||
|
|
350
|
+
normalized.startsWith("-") ||
|
|
351
|
+
normalized.includes("\\") ||
|
|
352
|
+
hasControlCharacter(normalized) ||
|
|
353
|
+
normalized.split("/").some((segment) => !segment || segment === "." || segment === "..")
|
|
354
|
+
) {
|
|
355
|
+
throw new Error("Invalid pi-sync settings: storage.path must be a safe relative path.");
|
|
356
|
+
}
|
|
357
|
+
return normalized;
|
|
389
358
|
}
|
|
390
359
|
|
|
391
360
|
export function normalizeS3Endpoint(value: string) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
return url.toString().replace(/\/$/u, "");
|
|
361
|
+
let url: URL;
|
|
362
|
+
try {
|
|
363
|
+
url = new URL(value.trim());
|
|
364
|
+
} catch {
|
|
365
|
+
throw new Error("Invalid pi-sync S3 endpoint.");
|
|
366
|
+
}
|
|
367
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
|
|
368
|
+
if (
|
|
369
|
+
(url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
|
|
370
|
+
url.username ||
|
|
371
|
+
url.password ||
|
|
372
|
+
url.search ||
|
|
373
|
+
url.hash
|
|
374
|
+
) {
|
|
375
|
+
throw new Error("Invalid pi-sync S3 endpoint: HTTPS is required except for loopback.");
|
|
376
|
+
}
|
|
377
|
+
url.pathname = url.pathname.replace(/\/+$/gu, "");
|
|
378
|
+
return url.toString().replace(/\/$/u, "");
|
|
411
379
|
}
|
|
412
380
|
|
|
413
381
|
export function normalizeS3Bucket(value: string | undefined) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
382
|
+
const bucket = requiredString(value, "S3 bucket");
|
|
383
|
+
if (bucket.includes("/") || bucket.includes("\\") || bucket.startsWith("-")) {
|
|
384
|
+
throw new Error("Invalid pi-sync S3 bucket.");
|
|
385
|
+
}
|
|
386
|
+
return bucket;
|
|
419
387
|
}
|
|
420
388
|
|
|
421
389
|
function hasControlCharacter(value: string) {
|
|
422
|
-
|
|
423
|
-
|
|
390
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Stored settings cannot contain controls.
|
|
391
|
+
return /[\u0000-\u001f\u007f-\u009f]/u.test(value);
|
|
424
392
|
}
|
|
425
393
|
|
|
426
394
|
export function isCloudflareR2Endpoint(endpoint: string | undefined) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
395
|
+
const value = endpoint?.trim();
|
|
396
|
+
if (!value) return false;
|
|
397
|
+
try {
|
|
398
|
+
const hostname = new URL(value).hostname.toLowerCase();
|
|
399
|
+
return hostname === "r2.cloudflarestorage.com" || hostname.endsWith(".r2.cloudflarestorage.com");
|
|
400
|
+
} catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
437
403
|
}
|
|
438
404
|
|
|
439
405
|
export function ownRecord(value: unknown): Record<string, unknown> | undefined {
|
|
440
|
-
|
|
441
|
-
? (value as Record<string, unknown>)
|
|
442
|
-
: undefined;
|
|
406
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
443
407
|
}
|