@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
|
@@ -10,295 +10,278 @@ export const MAX_GIT_SNAPSHOT_BYTES = 512 * 1024 * 1024;
|
|
|
10
10
|
const SNAPSHOT_VERSION = 1;
|
|
11
11
|
|
|
12
12
|
export interface GitManifestFile {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
path: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
size: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface GitManifest {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
version: number;
|
|
20
|
+
snapshotVersion: number;
|
|
21
|
+
snapshotId: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
machine: string;
|
|
24
|
+
profile: string;
|
|
25
|
+
syncSessions: boolean;
|
|
26
|
+
snapshotSyncSessions?: boolean;
|
|
27
|
+
selection?: SnapshotSelection;
|
|
28
|
+
files: GitManifestFile[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface PreparedGitFile extends GitManifestFile {
|
|
32
|
-
|
|
32
|
+
content: Buffer;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface GitTreeEntry {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
mode: string;
|
|
37
|
+
type: string;
|
|
38
|
+
object: string;
|
|
39
|
+
path: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function isGitPayloadSizeAllowed(size: number) {
|
|
43
|
-
|
|
43
|
+
return Number.isSafeInteger(size) && size >= 0 && size <= MAX_GIT_PAYLOAD_BYTES;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function requireGitManifest(value: unknown): GitManifest {
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return manifest as GitManifest;
|
|
47
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
48
|
+
throw new Error("Git publication manifest is malformed.");
|
|
49
|
+
}
|
|
50
|
+
const manifest = value as Partial<GitManifest>;
|
|
51
|
+
if (manifest.version === 1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Git publication uses the unsupported pre-release gzip format; recreate this pi-sync-owned test branch.",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
manifest.version !== GIT_MANIFEST_VERSION ||
|
|
58
|
+
manifest.snapshotVersion !== SNAPSHOT_VERSION ||
|
|
59
|
+
typeof manifest.snapshotId !== "string" ||
|
|
60
|
+
manifest.snapshotId.length > 512 ||
|
|
61
|
+
!/^[A-Za-z0-9._-]+$/u.test(manifest.snapshotId) ||
|
|
62
|
+
typeof manifest.createdAt !== "string" ||
|
|
63
|
+
manifest.createdAt.length > 64 ||
|
|
64
|
+
hasControlCharacter(manifest.createdAt) ||
|
|
65
|
+
Number.isNaN(Date.parse(manifest.createdAt)) ||
|
|
66
|
+
typeof manifest.machine !== "string" ||
|
|
67
|
+
manifest.machine.length > 256 ||
|
|
68
|
+
hasControlCharacter(manifest.machine) ||
|
|
69
|
+
typeof manifest.profile !== "string" ||
|
|
70
|
+
manifest.profile.length === 0 ||
|
|
71
|
+
manifest.profile.length > 256 ||
|
|
72
|
+
hasControlCharacter(manifest.profile) ||
|
|
73
|
+
typeof manifest.syncSessions !== "boolean" ||
|
|
74
|
+
(manifest.snapshotSyncSessions !== undefined && typeof manifest.snapshotSyncSessions !== "boolean") ||
|
|
75
|
+
!Array.isArray(manifest.files) ||
|
|
76
|
+
!hasExactKeys(manifest as Record<string, unknown>, [
|
|
77
|
+
"version",
|
|
78
|
+
"snapshotVersion",
|
|
79
|
+
"snapshotId",
|
|
80
|
+
"createdAt",
|
|
81
|
+
"machine",
|
|
82
|
+
"profile",
|
|
83
|
+
"syncSessions",
|
|
84
|
+
...(manifest.snapshotSyncSessions === undefined ? [] : ["snapshotSyncSessions"]),
|
|
85
|
+
...(manifest.selection === undefined ? [] : ["selection"]),
|
|
86
|
+
"files",
|
|
87
|
+
])
|
|
88
|
+
) {
|
|
89
|
+
throw new Error("Git publication manifest is malformed.");
|
|
90
|
+
}
|
|
91
|
+
if (manifest.selection !== undefined) portableSnapshotSelection(manifest.selection);
|
|
92
|
+
let total = 0;
|
|
93
|
+
const paths = new Set<string>();
|
|
94
|
+
for (const rawFile of manifest.files) {
|
|
95
|
+
if (!rawFile || typeof rawFile !== "object" || Array.isArray(rawFile)) {
|
|
96
|
+
throw new Error("Git publication manifest file is malformed.");
|
|
97
|
+
}
|
|
98
|
+
const file = rawFile as Partial<GitManifestFile>;
|
|
99
|
+
if (
|
|
100
|
+
!hasExactKeys(file as Record<string, unknown>, ["path", "sha256", "size"]) ||
|
|
101
|
+
!isSafeSnapshotPath(file.path) ||
|
|
102
|
+
typeof file.sha256 !== "string" ||
|
|
103
|
+
!/^[0-9a-f]{64}$/u.test(file.sha256) ||
|
|
104
|
+
typeof file.size !== "number" ||
|
|
105
|
+
!isGitPayloadSizeAllowed(file.size) ||
|
|
106
|
+
paths.has(file.path)
|
|
107
|
+
) {
|
|
108
|
+
throw new Error("Git publication manifest file is malformed.");
|
|
109
|
+
}
|
|
110
|
+
total += file.size;
|
|
111
|
+
if (!Number.isSafeInteger(total) || total > MAX_GIT_SNAPSHOT_BYTES) {
|
|
112
|
+
throw new Error(`Git snapshot content exceeds the ${MAX_GIT_SNAPSHOT_BYTES}-byte limit.`);
|
|
113
|
+
}
|
|
114
|
+
paths.add(file.path);
|
|
115
|
+
}
|
|
116
|
+
assertNoPathConflicts([...paths]);
|
|
117
|
+
return manifest as GitManifest;
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
export function validateGitSnapshot(snapshot: Snapshot, manifest: GitManifest, namespace: string) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
file.path !== expected.path ||
|
|
145
|
-
file.sha256 !== expected.sha256 ||
|
|
146
|
-
file.size !== expected.size
|
|
147
|
-
);
|
|
148
|
-
})
|
|
149
|
-
) {
|
|
150
|
-
throw new Error("Git snapshot identity does not match its publication manifest.");
|
|
151
|
-
}
|
|
121
|
+
const prepared = prepareGitSnapshot(snapshot, namespace);
|
|
122
|
+
const syncSessions =
|
|
123
|
+
snapshot.syncSessions === true || snapshot.files.some((file) => file.path.startsWith("sessions/"));
|
|
124
|
+
if (
|
|
125
|
+
snapshot.id !== manifest.snapshotId ||
|
|
126
|
+
snapshot.createdAt !== manifest.createdAt ||
|
|
127
|
+
snapshot.machine !== manifest.machine ||
|
|
128
|
+
snapshot.profile !== manifest.profile ||
|
|
129
|
+
snapshot.syncSessions !== manifest.snapshotSyncSessions ||
|
|
130
|
+
syncSessions !== manifest.syncSessions ||
|
|
131
|
+
!sameOptionalInclude(
|
|
132
|
+
snapshotSelectionInclude(snapshot),
|
|
133
|
+
manifest.selection === undefined ? undefined : portableSnapshotSelection(manifest.selection).include,
|
|
134
|
+
) ||
|
|
135
|
+
prepared.length !== manifest.files.length ||
|
|
136
|
+
prepared.some((file, index) => {
|
|
137
|
+
const expected = manifest.files[index];
|
|
138
|
+
return !expected || file.path !== expected.path || file.sha256 !== expected.sha256 || file.size !== expected.size;
|
|
139
|
+
})
|
|
140
|
+
) {
|
|
141
|
+
throw new Error("Git snapshot identity does not match its publication manifest.");
|
|
142
|
+
}
|
|
152
143
|
}
|
|
153
144
|
|
|
154
145
|
export function prepareGitSnapshot(snapshot: Snapshot, namespace: string) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
146
|
+
snapshotSelectionInclude(snapshot);
|
|
147
|
+
if (
|
|
148
|
+
snapshot.version !== SNAPSHOT_VERSION ||
|
|
149
|
+
typeof snapshot.id !== "string" ||
|
|
150
|
+
!snapshot.id ||
|
|
151
|
+
snapshot.id.length > 512 ||
|
|
152
|
+
!/^[A-Za-z0-9._-]+$/u.test(snapshot.id) ||
|
|
153
|
+
snapshot.profile !== namespace ||
|
|
154
|
+
!Array.isArray(snapshot.files) ||
|
|
155
|
+
typeof snapshot.createdAt !== "string" ||
|
|
156
|
+
!snapshot.createdAt ||
|
|
157
|
+
snapshot.createdAt.length > 64 ||
|
|
158
|
+
hasControlCharacter(snapshot.createdAt) ||
|
|
159
|
+
Number.isNaN(Date.parse(snapshot.createdAt)) ||
|
|
160
|
+
typeof snapshot.machine !== "string" ||
|
|
161
|
+
snapshot.machine.length > 256 ||
|
|
162
|
+
hasControlCharacter(snapshot.machine)
|
|
163
|
+
) {
|
|
164
|
+
throw new Error("Invalid Git snapshot publication.");
|
|
165
|
+
}
|
|
166
|
+
const paths = new Set<string>();
|
|
167
|
+
const prepared: PreparedGitFile[] = [];
|
|
168
|
+
let total = 0;
|
|
169
|
+
for (const file of snapshot.files) {
|
|
170
|
+
if (
|
|
171
|
+
!isSafeSnapshotPath(file.path) ||
|
|
172
|
+
typeof file.contentBase64 !== "string" ||
|
|
173
|
+
typeof file.sha256 !== "string" ||
|
|
174
|
+
!/^[0-9a-f]{64}$/u.test(file.sha256) ||
|
|
175
|
+
paths.has(file.path)
|
|
176
|
+
) {
|
|
177
|
+
throw new Error("Invalid Git snapshot file.");
|
|
178
|
+
}
|
|
179
|
+
const content = Buffer.from(file.contentBase64, "base64");
|
|
180
|
+
if (content.toString("base64") !== file.contentBase64 || sha256(content) !== file.sha256) {
|
|
181
|
+
throw new Error("Git snapshot file checksum mismatch.");
|
|
182
|
+
}
|
|
183
|
+
if (!isGitPayloadSizeAllowed(content.byteLength)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Git snapshot file exceeds GitHub's ${MAX_GIT_PAYLOAD_BYTES}-byte regular-Git limit: ${file.path}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
total += content.byteLength;
|
|
189
|
+
if (!Number.isSafeInteger(total) || total > MAX_GIT_SNAPSHOT_BYTES) {
|
|
190
|
+
throw new Error(`Git snapshot content exceeds the ${MAX_GIT_SNAPSHOT_BYTES}-byte limit.`);
|
|
191
|
+
}
|
|
192
|
+
paths.add(file.path);
|
|
193
|
+
prepared.push({ path: file.path, sha256: file.sha256, size: content.byteLength, content });
|
|
194
|
+
}
|
|
195
|
+
assertNoPathConflicts([...paths]);
|
|
196
|
+
return prepared;
|
|
206
197
|
}
|
|
207
198
|
|
|
208
199
|
export function parseGitTree(output: Buffer): GitTreeEntry[] {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
path: match.groups.path,
|
|
228
|
-
};
|
|
229
|
-
});
|
|
200
|
+
if (output.byteLength === 0) return [];
|
|
201
|
+
if (output.at(-1) !== 0) throw new Error("Git publication tree response is malformed.");
|
|
202
|
+
return output
|
|
203
|
+
.subarray(0, -1)
|
|
204
|
+
.toString("utf8")
|
|
205
|
+
.split("\0")
|
|
206
|
+
.map((line) => {
|
|
207
|
+
const match = /^(?<mode>[0-9]{6}) (?<type>blob|tree|commit) (?<object>[0-9a-f]{40})\t(?<path>.+)$/u.exec(line);
|
|
208
|
+
if (!match?.groups || hasControlCharacter(match.groups.path)) {
|
|
209
|
+
throw new Error("Git publication tree response is malformed.");
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
mode: match.groups.mode,
|
|
213
|
+
type: match.groups.type,
|
|
214
|
+
object: match.groups.object,
|
|
215
|
+
path: match.groups.path,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
230
218
|
}
|
|
231
219
|
|
|
232
220
|
export function validateGitPublicationTree(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
221
|
+
entries: GitTreeEntry[],
|
|
222
|
+
manifest: GitManifest,
|
|
223
|
+
manifestPath: string,
|
|
224
|
+
filePath: (path: string) => string,
|
|
237
225
|
) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
226
|
+
const byPath = new Map<string, GitTreeEntry>();
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
if (byPath.has(entry.path)) throw new Error("Git publication tree contains duplicate paths.");
|
|
229
|
+
byPath.set(entry.path, entry);
|
|
230
|
+
}
|
|
231
|
+
const expectedPaths = [manifestPath, ...manifest.files.map((file) => filePath(file.path))];
|
|
232
|
+
if (entries.length !== expectedPaths.length || expectedPaths.some((path) => !byPath.has(path))) {
|
|
233
|
+
throw new Error("Git publication tree has missing or extra files.");
|
|
234
|
+
}
|
|
235
|
+
for (const expectedPath of expectedPaths) {
|
|
236
|
+
const entry = byPath.get(expectedPath);
|
|
237
|
+
if (entry?.mode !== "100644" || entry.type !== "blob") {
|
|
238
|
+
throw new Error(`Git publication tree contains a non-regular file: ${expectedPath}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return manifest.files.map((file) => byPath.get(filePath(file.path)) as GitTreeEntry);
|
|
254
242
|
}
|
|
255
243
|
|
|
256
244
|
function sameOptionalInclude(left: string[] | undefined, right: string[] | undefined) {
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
if (!left || !right) return left === right;
|
|
246
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
259
247
|
}
|
|
260
248
|
|
|
261
249
|
function isSafeSnapshotPath(value: unknown): value is string {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
segment && segment !== "." && segment !== ".." && segment.toLowerCase() !== ".git",
|
|
274
|
-
)
|
|
275
|
-
);
|
|
250
|
+
return (
|
|
251
|
+
typeof value === "string" &&
|
|
252
|
+
value.length > 0 &&
|
|
253
|
+
value.length <= 4096 &&
|
|
254
|
+
!value.startsWith("/") &&
|
|
255
|
+
!value.includes("\\") &&
|
|
256
|
+
!hasControlCharacter(value) &&
|
|
257
|
+
value
|
|
258
|
+
.split("/")
|
|
259
|
+
.every((segment) => segment && segment !== "." && segment !== ".." && segment.toLowerCase() !== ".git")
|
|
260
|
+
);
|
|
276
261
|
}
|
|
277
262
|
|
|
278
263
|
function hasExactKeys(value: Record<string, unknown>, expected: string[]) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
keys.length === expectedKeys.length && keys.every((key, index) => key === expectedKeys[index])
|
|
283
|
-
);
|
|
264
|
+
const keys = Object.keys(value).sort();
|
|
265
|
+
const expectedKeys = [...expected].sort();
|
|
266
|
+
return keys.length === expectedKeys.length && keys.every((key, index) => key === expectedKeys[index]);
|
|
284
267
|
}
|
|
285
268
|
|
|
286
269
|
function assertNoPathConflicts(paths: string[]) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
270
|
+
const sorted = [...paths].sort();
|
|
271
|
+
for (let index = 1; index < sorted.length; index += 1) {
|
|
272
|
+
const parent = sorted[index - 1];
|
|
273
|
+
const child = sorted[index];
|
|
274
|
+
if (parent && child?.startsWith(`${parent}/`)) {
|
|
275
|
+
throw new Error(`Git snapshot file path conflict: ${parent} and ${child}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
295
278
|
}
|
|
296
279
|
|
|
297
280
|
function sha256(value: Buffer) {
|
|
298
|
-
|
|
281
|
+
return createHash("sha256").update(value).digest("hex");
|
|
299
282
|
}
|
|
300
283
|
|
|
301
284
|
function hasControlCharacter(value: string) {
|
|
302
|
-
|
|
303
|
-
|
|
285
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Reject untrusted terminal/ref controls.
|
|
286
|
+
return /[\u0000-\u001f\u007f-\u009f]/u.test(value);
|
|
304
287
|
}
|