terminus-cloud-settings-sync-plus 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +7 -0
- package/README.en.md +41 -0
- package/README.md +40 -2
- package/dist/index.js +2 -0
- package/dist/index.js.LICENSE.txt +7439 -0
- package/dist/src/components/change-logs/change-logs.component.d.ts +244 -0
- package/dist/src/components/checkbox.component.d.ts +13 -0
- package/dist/src/components/cloud-sync-settings.component.d.ts +431 -0
- package/dist/src/components/feeback-form/feeback.component.d.ts +242 -0
- package/dist/src/components/master-password/master-password.component.d.ts +244 -0
- package/dist/src/components/sub-components/about/about.component.d.ts +20 -0
- package/dist/src/components/sub-components/amazon/amazon-settings.component.d.ts +459 -0
- package/dist/src/components/sub-components/built-in/builtin-settings.component.d.ts +444 -0
- package/dist/src/components/sub-components/check-for-updates/check-for-updates.component.d.ts +267 -0
- package/dist/src/components/sub-components/dropbox/dropbox-encryption-secret-modal.component.d.ts +252 -0
- package/dist/src/components/sub-components/dropbox/dropbox-settings.component.d.ts +488 -0
- package/dist/src/components/sub-components/ftp/ftp-settings.component.d.ts +460 -0
- package/dist/src/components/sub-components/gist/gist-settings.component.d.ts +461 -0
- package/dist/src/components/sub-components/webdav/webdav-settings.component.d.ts +454 -0
- package/dist/src/components/toggle.component.d.ts +4 -0
- package/dist/src/data/lang.d.ts +245 -0
- package/dist/src/data/s3-data.d.ts +15 -0
- package/dist/src/data/setting-items.d.ts +198 -0
- package/dist/src/index.d.ts +47 -0
- package/dist/src/interface/index.d.ts +65 -0
- package/dist/src/providers/cloud-sync-actions.d.ts +15 -0
- package/dist/src/providers/cloud-sync-command.provider.d.ts +16 -0
- package/dist/src/providers/cloud-sync-toolbar.provider.d.ts +7 -0
- package/dist/src/services/dev-constants.d.ts +4 -0
- package/dist/src/services/tabby-sync-upgrade.d.ts +10 -0
- package/dist/src/services/toast.d.ts +61 -0
- package/dist/src/settings.d.ts +7 -0
- package/dist/src/utils/CloudSyncSettingsHelper.d.ts +8 -0
- package/dist/src/utils/Logger.d.ts +29 -0
- package/dist/src/utils/cloud-components/AmazonS3.d.ts +38 -0
- package/dist/src/utils/cloud-components/Dropbox.d.ts +50 -0
- package/dist/src/utils/cloud-components/FTP.d.ts +46 -0
- package/dist/src/utils/cloud-components/WebDav.d.ts +34 -0
- package/dist/src/utils/cloud-components/gist.d.ts +86 -0
- package/dist/src/utils/cloud-components/gists/gists.d.ts +15 -0
- package/dist/src/utils/cloud-components/gists/gitee.d.ts +16 -0
- package/dist/src/utils/cloud-components/gists/github.d.ts +16 -0
- package/dist/src/utils/cloud-components/gists/gitlab.d.ts +44 -0
- package/dist/src/utils/cloud-components/sync-utils.d.ts +90 -0
- package/dist/src/utils/config-hash.d.ts +21 -0
- package/dist/src/utils/dev-mock-data.d.ts +6 -0
- package/dist/src/utils/settings-crypto.d.ts +53 -0
- package/dist/src/utils/settings-helper.d.ts +177 -0
- package/dist/src/utils/settings-secret.d.ts +16 -0
- package/dist/src/utils/sync-lock.d.ts +44 -0
- package/dist/src/utils/sync-payload.d.ts +38 -0
- package/dist/src/utils/sync-state.d.ts +55 -0
- package/package.json +111 -6
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ConfigService, PlatformService } from 'terminus-core';
|
|
2
|
+
import Logger from '../Logger';
|
|
3
|
+
import { GistParams, SyncResult } from '../../interface';
|
|
4
|
+
/**
|
|
5
|
+
* Base class shared by the "files map" style Gist providers (GitHub, Gitee).
|
|
6
|
+
*
|
|
7
|
+
* Both providers store one or more named files inside a single gist and expose
|
|
8
|
+
* almost identical sync flows; the only differences are the auth headers and
|
|
9
|
+
* the connection-test request, which subclasses override.
|
|
10
|
+
*/
|
|
11
|
+
declare class Gist {
|
|
12
|
+
protected baseRequestUrl: string;
|
|
13
|
+
protected id: string;
|
|
14
|
+
protected accessToken: string;
|
|
15
|
+
constructor(url: string, id: string, accessToken: string);
|
|
16
|
+
/**
|
|
17
|
+
* Key under which this provider's sync baseline is stored.
|
|
18
|
+
*
|
|
19
|
+
* All gist flavours share the single stored adapter id (`gists`), so the
|
|
20
|
+
* gist type is appended: switching from GitHub to GitLab keeps the same
|
|
21
|
+
* adapter and would otherwise inherit the other provider's baseline.
|
|
22
|
+
*/
|
|
23
|
+
protected getAdapterId(): string;
|
|
24
|
+
/** Short provider discriminator used to key the baseline. */
|
|
25
|
+
protected getGistType(): string;
|
|
26
|
+
/** A dated description used when writing gist revisions. */
|
|
27
|
+
getSyncTextDateTime(): string;
|
|
28
|
+
/** Placeholder content used when creating an empty gist. */
|
|
29
|
+
getDummyContent(): string;
|
|
30
|
+
/** HTTP headers used to authenticate gist requests. Overridden per provider. */
|
|
31
|
+
protected getAuthHeaders(): any;
|
|
32
|
+
/** Fetch the raw gist payload, returning `{ code, data, message }`. */
|
|
33
|
+
protected fetchGist(url: string, logger: Logger): Promise<{
|
|
34
|
+
code: number;
|
|
35
|
+
data: any;
|
|
36
|
+
message: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** Extract the file map and the first file's content from a gist payload. */
|
|
39
|
+
private extractFiles;
|
|
40
|
+
/**
|
|
41
|
+
* Two-way sync between the local Tabby config and a gist.
|
|
42
|
+
*
|
|
43
|
+
* @param firstInit When `true` prompts the user to choose the initial sync
|
|
44
|
+
* direction. Otherwise {@link resolveSyncDirection} compares content
|
|
45
|
+
* hashes against the recorded baseline to decide.
|
|
46
|
+
*/
|
|
47
|
+
sync: (config: ConfigService, platform: PlatformService, params: GistParams, firstInit?: boolean) => Promise<SyncResult>;
|
|
48
|
+
/** Human readable provider name used in log output. Overridden per provider. */
|
|
49
|
+
protected getProviderLabel(): string;
|
|
50
|
+
/** Build a new instance of the concrete provider. Overridden per provider. */
|
|
51
|
+
protected createComponent(id: string, accessToken: string): Gist;
|
|
52
|
+
/**
|
|
53
|
+
* Push the local Tabby config to every file inside the gist.
|
|
54
|
+
*
|
|
55
|
+
* The `description` field is bumped on every write, and GitHub/Gitee treat
|
|
56
|
+
* that as a revision, so `updated_at` moves even when the content is
|
|
57
|
+
* unchanged. That is why the upload is skipped when the gist already holds
|
|
58
|
+
* this exact config: otherwise the remote timestamp would keep advancing and
|
|
59
|
+
* every device would believe the cloud had newer settings.
|
|
60
|
+
*
|
|
61
|
+
* @param gistFiles Known file map to overwrite; fetched from the server
|
|
62
|
+
* when not provided (e.g. a forced upload from the UI).
|
|
63
|
+
* @param remoteContent Content already known to be on the server, used to
|
|
64
|
+
* skip a pointless write. Fetched alongside `gistFiles` when omitted.
|
|
65
|
+
*/
|
|
66
|
+
protected uploadLocalSettings(platform: PlatformService, gistFiles?: any, remoteContent?: string): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the remote copy already contains the config we are about to
|
|
69
|
+
* upload.
|
|
70
|
+
*
|
|
71
|
+
* Both sides are decrypted first: `CryptoJS.AES.encrypt` salts every call,
|
|
72
|
+
* so the ciphertext of identical input differs and comparing the encrypted
|
|
73
|
+
* payloads would always report a change.
|
|
74
|
+
*/
|
|
75
|
+
protected isRemoteUpToDate(remoteContent: string, localContent: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Force-push the local Tabby config to the gist.
|
|
78
|
+
*
|
|
79
|
+
* Concurrency is handled by the shared lock in `SettingsHelper`, so there is
|
|
80
|
+
* no adapter-local re-entrancy flag here any more; the old per-adapter flags
|
|
81
|
+
* could silently turn a scheduled push into a no-op that still reported
|
|
82
|
+
* success.
|
|
83
|
+
*/
|
|
84
|
+
syncLocalSettingsToCloud(platform: PlatformService): Promise<boolean>;
|
|
85
|
+
}
|
|
86
|
+
export default Gist;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ConfigService, PlatformService } from 'terminus-core';
|
|
2
|
+
import { GistParams, SyncResult } from '../../../interface';
|
|
3
|
+
/**
|
|
4
|
+
* Facade that dispatches gist sync operations to the correct provider based on
|
|
5
|
+
* the stored `type` (github/gitee/gitlab).
|
|
6
|
+
*/
|
|
7
|
+
declare class Gists {
|
|
8
|
+
private factories;
|
|
9
|
+
/** Instantiate the provider component for a given gist type. */
|
|
10
|
+
private createComponent;
|
|
11
|
+
sync: (config: ConfigService, platform: PlatformService, params: GistParams, firstInit?: boolean) => Promise<SyncResult>;
|
|
12
|
+
syncLocalSettingsToCloud: (platform: PlatformService) => Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
declare const _default: Gists;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PlatformService } from 'terminus-core';
|
|
2
|
+
import Gist from '../gist';
|
|
3
|
+
declare class Gitee extends Gist {
|
|
4
|
+
constructor(id: string, accessToken: string);
|
|
5
|
+
protected getProviderLabel(): string;
|
|
6
|
+
protected getGistType(): string;
|
|
7
|
+
protected getAuthHeaders(): any;
|
|
8
|
+
protected createComponent(id: string, accessToken: string): Gist;
|
|
9
|
+
/**
|
|
10
|
+
* Verify the token, creating a new gist when no id is configured.
|
|
11
|
+
*
|
|
12
|
+
* @return `{ code: 1, data }` on success or `{ code: 0, message }`.
|
|
13
|
+
*/
|
|
14
|
+
testConnection: (platform: PlatformService) => Promise<any>;
|
|
15
|
+
}
|
|
16
|
+
export default Gitee;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PlatformService } from 'terminus-core';
|
|
2
|
+
import Gist from '../gist';
|
|
3
|
+
declare class Github extends Gist {
|
|
4
|
+
constructor(id: string, accessToken: string);
|
|
5
|
+
protected getProviderLabel(): string;
|
|
6
|
+
protected getGistType(): string;
|
|
7
|
+
protected getAuthHeaders(): any;
|
|
8
|
+
protected createComponent(id: string, accessToken: string): Gist;
|
|
9
|
+
/**
|
|
10
|
+
* Verify the token, creating a new gist when no id is configured.
|
|
11
|
+
*
|
|
12
|
+
* @return `{ code: 1, data }` on success or `{ code: 0, message }`.
|
|
13
|
+
*/
|
|
14
|
+
testConnection: (platform: PlatformService) => Promise<any>;
|
|
15
|
+
}
|
|
16
|
+
export default Github;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ConfigService, PlatformService } from 'terminus-core';
|
|
2
|
+
import Gist from '../gist';
|
|
3
|
+
import { GistParams, SyncResult } from '../../../interface';
|
|
4
|
+
/**
|
|
5
|
+
* GitLab snippet provider. Unlike GitHub/Gitee this stores a single raw text
|
|
6
|
+
* snippet, so it overrides the file-map based sync from {@link Gist}.
|
|
7
|
+
*/
|
|
8
|
+
declare class Gitlab extends Gist {
|
|
9
|
+
constructor(id: string, accessToken: string);
|
|
10
|
+
protected getProviderLabel(): string;
|
|
11
|
+
protected getGistType(): string;
|
|
12
|
+
protected getAuthHeaders(): any;
|
|
13
|
+
/**
|
|
14
|
+
* Verify the token, creating a new snippet when no id is configured.
|
|
15
|
+
*
|
|
16
|
+
* @return `{ code: 1, data }` on success or `{ code: 0, message }`.
|
|
17
|
+
*/
|
|
18
|
+
testConnection: (platform: PlatformService) => Promise<any>;
|
|
19
|
+
/** Name of the single file held inside the snippet. */
|
|
20
|
+
private static readonly SNIPPET_FILE_PATH;
|
|
21
|
+
/**
|
|
22
|
+
* Two-way sync between the local Tabby config and a GitLab snippet.
|
|
23
|
+
*
|
|
24
|
+
* @param firstInit When `true` prompts the user to choose the initial sync
|
|
25
|
+
* direction. Otherwise {@link resolveSyncDirection} compares content
|
|
26
|
+
* hashes against the recorded baseline to decide.
|
|
27
|
+
*/
|
|
28
|
+
sync: (config: ConfigService, platform: PlatformService, params: GistParams, firstInit?: boolean) => Promise<SyncResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Overwrite the snippet with the local config.
|
|
31
|
+
*
|
|
32
|
+
* As with GitHub, `description` is bumped on every write and GitLab moves
|
|
33
|
+
* `updated_at` accordingly, so an unchanged config is deliberately not
|
|
34
|
+
* re-uploaded; otherwise the snippet would look permanently newer than
|
|
35
|
+
* every device's local copy.
|
|
36
|
+
*
|
|
37
|
+
* @param _gistFiles Unused here (GitLab stores a single file), accepted only
|
|
38
|
+
* to keep the signature compatible with {@link Gist}.
|
|
39
|
+
* @param remoteContent Content already known to be on the server, fetched
|
|
40
|
+
* when omitted so a forced upload from the UI can still skip a no-op write.
|
|
41
|
+
*/
|
|
42
|
+
protected uploadLocalSettings(platform: PlatformService, _gistFiles?: any, remoteContent?: string): Promise<boolean>;
|
|
43
|
+
}
|
|
44
|
+
export default Gitlab;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ConfigService, PlatformService } from 'terminus-core';
|
|
2
|
+
import { hashConfigContent } from '../config-hash';
|
|
3
|
+
import Logger from '../Logger';
|
|
4
|
+
import moment from 'moment';
|
|
5
|
+
/** The concrete operation a sync cycle decided to perform. */
|
|
6
|
+
export declare type SyncAction = 'noop' | 'pull' | 'push' | 'conflict' | 'abort';
|
|
7
|
+
/**
|
|
8
|
+
* Options describing how a single adapter should decide its sync direction.
|
|
9
|
+
*/
|
|
10
|
+
export interface SyncDirectionOptions {
|
|
11
|
+
config: ConfigService;
|
|
12
|
+
platform: PlatformService;
|
|
13
|
+
logger: Logger;
|
|
14
|
+
/** Human readable adapter name used in the log output. */
|
|
15
|
+
providerLabel: string;
|
|
16
|
+
/** Stored provider id, used to key the persisted sync baseline. */
|
|
17
|
+
adapterId: string;
|
|
18
|
+
/** Remote config `moment` timestamp, or `null` when it is unknown. */
|
|
19
|
+
remoteUpdatedAt: moment.Moment | null;
|
|
20
|
+
/** Raw (still encrypted) config content downloaded from the cloud. */
|
|
21
|
+
remoteContent: string;
|
|
22
|
+
/**
|
|
23
|
+
* Callback invoked to push the local config to the cloud. May resolve to a
|
|
24
|
+
* `SyncResult`, a bare boolean, or nothing; all three are understood.
|
|
25
|
+
*/
|
|
26
|
+
pushToCloud: () => any;
|
|
27
|
+
}
|
|
28
|
+
/** Outcome of a sync cycle, reported back to the adapter and the UI. */
|
|
29
|
+
export interface SyncOutcome {
|
|
30
|
+
action: SyncAction;
|
|
31
|
+
result: boolean;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether a failed remote read means "there is no config on the server yet"
|
|
36
|
+
* rather than "the server could not be reached".
|
|
37
|
+
*
|
|
38
|
+
* The adapters used to upload the local config whenever *any* read error
|
|
39
|
+
* occurred, so a transient 500 or a dropped connection silently published a
|
|
40
|
+
* possibly stale local config over a good remote one. Only a genuine
|
|
41
|
+
* not-found is safe to treat that way.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isRemoteMissingError(error: any): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Fingerprint config content so two sides can be compared by value.
|
|
46
|
+
*
|
|
47
|
+
* Re-exported from {@link hashConfigContent} so adapters only need to import
|
|
48
|
+
* from this module.
|
|
49
|
+
*/
|
|
50
|
+
export { hashConfigContent };
|
|
51
|
+
/**
|
|
52
|
+
* Apply a validated cloud config to the local file during the interactive
|
|
53
|
+
* first-init flow, recording the baseline so the first background cycle sees
|
|
54
|
+
* both sides as already in agreement.
|
|
55
|
+
*
|
|
56
|
+
* Without this the very next cycle would compare a fresh local mtime against
|
|
57
|
+
* the older remote timestamp and immediately push back what was just pulled.
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyRemoteConfigOnFirstInit(config: ConfigService, platform: PlatformService, adapterId: string, remoteContent: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Record the baseline after the interactive first-init flow pushed the local
|
|
62
|
+
* config to the cloud, for the same reason as
|
|
63
|
+
* {@link applyRemoteConfigOnFirstInit}.
|
|
64
|
+
*/
|
|
65
|
+
export declare function recordLocalBaselineOnFirstInit(platform: PlatformService, adapterId: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Decide what to do when local and remote content differ.
|
|
68
|
+
*
|
|
69
|
+
* With a baseline we know which side moved, which is the only way to tell a
|
|
70
|
+
* one-sided edit from a genuine two-sided conflict. Without one we fall back to
|
|
71
|
+
* timestamps, but only when the gap is larger than the configured tolerance so
|
|
72
|
+
* that clock skew and second-granularity remote timestamps (FTP `MDTM`, WebDav
|
|
73
|
+
* `lastmod`) cannot decide the outcome on their own.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isTrueConflict(localHash: string, remoteHash: string, baselineHash: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Decide and perform the sync for one polling cycle.
|
|
78
|
+
*
|
|
79
|
+
* The algorithm is content-first: the decrypted cloud config is compared to the
|
|
80
|
+
* local one by hash, so an unchanged install transfers nothing at all. This is
|
|
81
|
+
* what stops the pull/push ping-pong the previous timestamp-only version was
|
|
82
|
+
* guaranteed to produce, where writing the pulled file bumped the local mtime,
|
|
83
|
+
* which made the next cycle push, which bumped the remote mtime, which made the
|
|
84
|
+
* cycle after that pull again — forever.
|
|
85
|
+
*
|
|
86
|
+
* Timestamps are now only a tiebreaker for the very first cycle on a device,
|
|
87
|
+
* and the remote payload is validated before it is ever allowed to overwrite
|
|
88
|
+
* the local config.
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveSyncDirection(options: SyncDirectionOptions): Promise<SyncOutcome>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PlatformService } from 'terminus-core';
|
|
2
|
+
/**
|
|
3
|
+
* Fingerprint config content so two sides can be compared by value.
|
|
4
|
+
*
|
|
5
|
+
* The YAML is parsed and re-serialised in a canonical form before hashing, so
|
|
6
|
+
* two configs that mean the same thing hash the same even when their formatting
|
|
7
|
+
* differs. That matters because Tabby rewrites `config.yaml` through its own
|
|
8
|
+
* `js-yaml` dump after every change: comparing raw text would report a
|
|
9
|
+
* difference after each pull and trigger a pointless push back to the cloud.
|
|
10
|
+
* Content that is not valid YAML falls back to a whitespace-normalised text
|
|
11
|
+
* hash.
|
|
12
|
+
*
|
|
13
|
+
* Note that comparing the *encrypted* payloads would never work:
|
|
14
|
+
* `CryptoJS.AES.encrypt` salts every call, so identical input produces
|
|
15
|
+
* different ciphertext each time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hashConfigContent(content: string): string;
|
|
18
|
+
/** Hash of the Tabby config currently on disk, or `''` when it cannot be read. */
|
|
19
|
+
export declare function getLocalConfigHash(platform: PlatformService): string;
|
|
20
|
+
/** First 8 characters of a hash, enough to correlate log lines. */
|
|
21
|
+
export declare function shortHash(hash: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** The JSON envelope version used by the authenticated encryption format. */
|
|
2
|
+
export declare const ENCRYPTION_VERSION = 2;
|
|
3
|
+
/**
|
|
4
|
+
* The passphrase used by the original plugin encryption format.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated V1 is retained only for decrypting and migrating existing data.
|
|
7
|
+
* Use a user-provided passphrase with `encryptV2` for new data.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LEGACY_ENCRYPTION_KEY = "tp!&nc3^to8y7^3#4%2%&szufx!";
|
|
10
|
+
export interface V2EncryptionEnvelope {
|
|
11
|
+
format: 'tabby-sync';
|
|
12
|
+
version: 2;
|
|
13
|
+
algorithm: 'aes-256-gcm';
|
|
14
|
+
kdf: 'scrypt';
|
|
15
|
+
salt: string;
|
|
16
|
+
iv: string;
|
|
17
|
+
authTag: string;
|
|
18
|
+
ciphertext: string;
|
|
19
|
+
keyLength: 32;
|
|
20
|
+
cost: number;
|
|
21
|
+
blockSize: number;
|
|
22
|
+
parallelization: number;
|
|
23
|
+
}
|
|
24
|
+
export declare type ParsedPayload = {
|
|
25
|
+
format: 'v1';
|
|
26
|
+
ciphertext: string;
|
|
27
|
+
} | {
|
|
28
|
+
format: 'v2';
|
|
29
|
+
envelope: V2EncryptionEnvelope;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Returns true when a payload is a V2 envelope, including an envelope whose
|
|
33
|
+
* fields are malformed. This lets callers fail closed instead of trying V1.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isV2Payload(payload: string): boolean;
|
|
36
|
+
/** Detects and validates the outer payload format without decrypting it. */
|
|
37
|
+
export declare function parsePayload(payload: string): ParsedPayload;
|
|
38
|
+
/**
|
|
39
|
+
* Encrypts using the original CryptoJS passphrase AES format.
|
|
40
|
+
* @deprecated Use `encryptV2` for new payloads.
|
|
41
|
+
*/
|
|
42
|
+
export declare function encryptV1(plaintext: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Decrypts the original CryptoJS passphrase AES format.
|
|
45
|
+
* @deprecated Use `decryptV2` for V2 payloads.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decryptV1(ciphertext: string): string;
|
|
48
|
+
/** Encrypts non-empty plaintext with scrypt-derived AES-256-GCM. */
|
|
49
|
+
export declare function encryptV2(plaintext: string, passphrase: string): string;
|
|
50
|
+
/** Decrypts and authenticates a V2 JSON envelope. */
|
|
51
|
+
export declare function decryptV2(payload: string, passphrase: string): string;
|
|
52
|
+
/** Decrypts V2 payloads strictly, falling back to V1 only for non-V2 data. */
|
|
53
|
+
export declare function decryptPayload(payload: string, passphrase?: string): string;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ConfigService, PlatformService } from 'terminus-core';
|
|
2
|
+
import { EventEmitter } from '@angular/core';
|
|
3
|
+
export declare class SettingsHelperClass {
|
|
4
|
+
/**
|
|
5
|
+
* Maps a stored provider identifier to the adapter instance that knows how
|
|
6
|
+
* to talk to that cloud service.
|
|
7
|
+
*/
|
|
8
|
+
private adapterHandler;
|
|
9
|
+
/**
|
|
10
|
+
* Absolute path of the encrypted plugin settings file (stored next to the
|
|
11
|
+
* Tabby config directory).
|
|
12
|
+
*/
|
|
13
|
+
private getStoredSettingsPath;
|
|
14
|
+
/**
|
|
15
|
+
* Encrypt an arbitrary payload (usually the stored settings object) with
|
|
16
|
+
* the plugin header prefix so it can be recognised later.
|
|
17
|
+
*/
|
|
18
|
+
private encryptPayload;
|
|
19
|
+
/**
|
|
20
|
+
* Promise wrapper around `fs.writeFile` that resolves to `true` on success
|
|
21
|
+
* and `false` on failure. Centralising this fixes the historical bug where
|
|
22
|
+
* the write callback rejected without returning, so `resolve(true)` still
|
|
23
|
+
* ran after an error.
|
|
24
|
+
*/
|
|
25
|
+
private writeFileAsync;
|
|
26
|
+
/**
|
|
27
|
+
* Encrypt `payload` and persist it to `filePath`. Returns `true` when the
|
|
28
|
+
* file was written successfully.
|
|
29
|
+
*/
|
|
30
|
+
private writeEncryptedConfig;
|
|
31
|
+
private writeEncryptedConfigAtomically;
|
|
32
|
+
private getSshConfigPath;
|
|
33
|
+
getSshConfigFilePath(): string;
|
|
34
|
+
private readSshConfig;
|
|
35
|
+
getLocalSshConfigContent(): string;
|
|
36
|
+
private getExcludedSshCacheKey;
|
|
37
|
+
private readExcludedSshBlocks;
|
|
38
|
+
private writeExcludedSshBlocks;
|
|
39
|
+
/** Read the combined, backward-compatible payload that is encrypted and stored in the cloud. */
|
|
40
|
+
readSyncContent(platform: PlatformService, encrypted?: boolean): string;
|
|
41
|
+
/** Fingerprint both synced files so SSH-only edits trigger the existing sync cycle. */
|
|
42
|
+
getLocalSyncHash(platform: PlatformService): string;
|
|
43
|
+
getLocalSshConfigHash(): string;
|
|
44
|
+
/** Back up the OpenSSH config before applying a cloud pull. */
|
|
45
|
+
backupSshConfigFile(): Promise<boolean>;
|
|
46
|
+
/** Replace the local OpenSSH config after the caller has created a backup. */
|
|
47
|
+
applySshConfigFromSync(cloudSshConfig: string): Promise<boolean>;
|
|
48
|
+
writeSshConflictSnapshot(content: string): Promise<string>;
|
|
49
|
+
/** Apply a cloud payload, preserving old single-file cloud content. */
|
|
50
|
+
applySyncContent(config: ConfigService, platform: PlatformService, content: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Persist the provider configuration to disk. Existing enable/loader/
|
|
53
|
+
* interval preferences are preserved when a settings file already exists.
|
|
54
|
+
*/
|
|
55
|
+
saveSettingsToFile(platform: PlatformService, adapter: string, params: any): Promise<boolean>;
|
|
56
|
+
/**
|
|
57
|
+
* Encrypt the complete local sync payload and write it to FTP's temporary file.
|
|
58
|
+
*/
|
|
59
|
+
generateEncryptedTabbyFileForUpload(platform: PlatformService): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Run the two-way sync for the currently configured adapter.
|
|
62
|
+
*
|
|
63
|
+
* Wrapped in the shared {@link SyncLock} so the periodic loop, the
|
|
64
|
+
* config-change listener and the settings UI can never overlap. When the
|
|
65
|
+
* lock is unavailable the call reports failure with an explanatory message
|
|
66
|
+
* instead of silently doing nothing.
|
|
67
|
+
*
|
|
68
|
+
* @param firstInit When `true` the adapter prompts the user to choose the
|
|
69
|
+
* initial sync direction (cloud → local or local → cloud).
|
|
70
|
+
* @returns `{ result, message }` describing whether the sync succeeded.
|
|
71
|
+
*/
|
|
72
|
+
syncWithCloud(config: ConfigService, platform: PlatformService, firstInit?: boolean, emitter?: EventEmitter<any>): Promise<any>;
|
|
73
|
+
/**
|
|
74
|
+
* Force-push the local Tabby config to the cloud using the saved adapter.
|
|
75
|
+
*
|
|
76
|
+
* Shares the same lock as {@link syncWithCloud}; when a cycle is already
|
|
77
|
+
* running the push is skipped rather than racing it.
|
|
78
|
+
*/
|
|
79
|
+
syncLocalSettingsToCloud(platform: PlatformService): Promise<boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* Read and decrypt the stored plugin settings file.
|
|
82
|
+
*
|
|
83
|
+
* @param isRaw When `true` returns the decrypted string instead of a parsed
|
|
84
|
+
* object.
|
|
85
|
+
* @returns The parsed {@link StoredSettings}, the raw string, or `null`
|
|
86
|
+
* when the file is missing or cannot be decrypted.
|
|
87
|
+
*/
|
|
88
|
+
readConfigFile(platform: PlatformService, isRaw?: boolean): any;
|
|
89
|
+
/**
|
|
90
|
+
* Read the local Tabby `config.yaml`.
|
|
91
|
+
*
|
|
92
|
+
* @param isRaw When `true` returns the file content as a string.
|
|
93
|
+
* @param isEncrypt When `true` (and `isRaw` is set) returns the encrypted,
|
|
94
|
+
* header-prefixed content ready to upload to the cloud.
|
|
95
|
+
*/
|
|
96
|
+
readTabbyConfigFile(platform: PlatformService, isRaw?: boolean, isEncrypt?: boolean): any;
|
|
97
|
+
/**
|
|
98
|
+
* Create a `.backup` copy of the local Tabby config before it is
|
|
99
|
+
* overwritten by a cloud pull.
|
|
100
|
+
*/
|
|
101
|
+
backupTabbyConfigFile(platform: PlatformService): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Overwrite the local Tabby `config.yaml` with plain (already decrypted)
|
|
104
|
+
* YAML content.
|
|
105
|
+
*
|
|
106
|
+
* Used before handing the same content to `ConfigService.writeRaw`, which
|
|
107
|
+
* kicks off a `save()` and a `load()` concurrently without awaiting either.
|
|
108
|
+
* If the `load()` wins the race it reads the *old* file and reverts Tabby's
|
|
109
|
+
* in-memory store, silently undoing the pull. Writing the file first makes
|
|
110
|
+
* both orderings produce the same result.
|
|
111
|
+
*/
|
|
112
|
+
writeTabbyConfigFile(platform: PlatformService, content: string): Promise<boolean>;
|
|
113
|
+
/**
|
|
114
|
+
* Save the cloud version of the config next to the local one when both sides
|
|
115
|
+
* changed, so a conflict never destroys data.
|
|
116
|
+
*
|
|
117
|
+
* @returns The path written, or `null` when the write failed.
|
|
118
|
+
*/
|
|
119
|
+
writeConflictSnapshot(platform: PlatformService, content: string): Promise<string>;
|
|
120
|
+
/**
|
|
121
|
+
* Write a timestamped recovery snapshot and retain only the newest entries.
|
|
122
|
+
* The existing `config.yaml.backup` file is intentionally left untouched.
|
|
123
|
+
*/
|
|
124
|
+
writeSnapshot(platform: PlatformService, content: string, kind?: string, retention?: number): Promise<string>;
|
|
125
|
+
/** List newest-to-oldest timestamped snapshots for the local config. */
|
|
126
|
+
listSnapshots(platform: PlatformService, kind?: string): string[];
|
|
127
|
+
/** Restore a selected snapshot into `config.yaml`, preserving `.backup`. */
|
|
128
|
+
restoreSnapshot(platform: PlatformService, snapshotPath: string): Promise<boolean>;
|
|
129
|
+
private pruneSnapshots;
|
|
130
|
+
/**
|
|
131
|
+
* Patch one or more fields of the stored settings file and persist it,
|
|
132
|
+
* surfacing a success/error toast. Shared by the interval and toggle
|
|
133
|
+
* setters.
|
|
134
|
+
*/
|
|
135
|
+
private updateSavedConfig;
|
|
136
|
+
/** Persist the auto-sync interval (in seconds). */
|
|
137
|
+
saveIntervalSync(value: number, platform: PlatformService): Promise<boolean>;
|
|
138
|
+
/** Enable or disable the auto-sync feature. */
|
|
139
|
+
toggleEnabledPlugin(value: boolean, platform: PlatformService): Promise<boolean>;
|
|
140
|
+
/** Show or hide the syncing loader indicator. */
|
|
141
|
+
toggleEnabledShowLoader(value: boolean, platform: PlatformService): Promise<boolean>;
|
|
142
|
+
/**
|
|
143
|
+
* Remove the saved plugin settings file, optionally prompting the user for
|
|
144
|
+
* confirmation first.
|
|
145
|
+
*/
|
|
146
|
+
removeConfirmFile(platform: PlatformService, needConfirm?: boolean, showSuccess?: boolean): Promise<boolean>;
|
|
147
|
+
/** Delete the stored settings file from disk. */
|
|
148
|
+
_removeSavedConfig(platform: PlatformService): boolean;
|
|
149
|
+
/** Remove local encrypted artifacts that belong to the disconnected provider. */
|
|
150
|
+
private removeDisconnectedProviderArtifacts;
|
|
151
|
+
/** Return the encrypted payload after removing the plugin file header. */
|
|
152
|
+
private getEncryptedPayload;
|
|
153
|
+
/** Decrypt a header-prefixed cloud config string back to plain text. */
|
|
154
|
+
doDescryption(content: string): string;
|
|
155
|
+
/** Detect whether a downloaded plugin config uses the authenticated V2 envelope. */
|
|
156
|
+
isV2EncryptedConfig(content: string): boolean;
|
|
157
|
+
/** Return whether the current keychain secret can decrypt this config. */
|
|
158
|
+
canDecryptConfig(content: string): boolean;
|
|
159
|
+
/** Verify a candidate secret without storing it or modifying local configuration. */
|
|
160
|
+
verifyCustomEncryptionSecret(content: string, secret: string): boolean;
|
|
161
|
+
/** Verify and persist the secret needed to unlock an existing remote V2 config. */
|
|
162
|
+
unlockCustomEncryptionSecret(platform: PlatformService, content: string, secret: string): Promise<boolean>;
|
|
163
|
+
/** Check whether a downloaded config was produced by this plugin. */
|
|
164
|
+
verifyServerConfigIsValid(configRawData: string): boolean;
|
|
165
|
+
/** Clear the persisted `lastErrorMessage` for the given adapter config. */
|
|
166
|
+
clearLastErrorMessage(platform: PlatformService, adapter: string, params: any): void;
|
|
167
|
+
/**
|
|
168
|
+
* Fetch remote plugin settings (currently the Dropbox API credentials) on
|
|
169
|
+
* startup and merge them into the in-memory form data.
|
|
170
|
+
*/
|
|
171
|
+
loadEncryptionSecret(): Promise<void>;
|
|
172
|
+
hasCustomEncryptionSecret(): boolean;
|
|
173
|
+
setCustomEncryptionSecret(platform: PlatformService, secret: string): Promise<boolean>;
|
|
174
|
+
clearCustomEncryptionSecret(): Promise<boolean>;
|
|
175
|
+
}
|
|
176
|
+
declare const _default: SettingsHelperClass;
|
|
177
|
+
export default _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const SYNC_SECRET_SERVICE = "tabby-cloud-sync-settings";
|
|
2
|
+
export declare const SYNC_SECRET_ACCOUNT = "encryption-passphrase";
|
|
3
|
+
/** OS keychain storage for the optional V2 sync passphrase. */
|
|
4
|
+
declare class SettingsSecretStore {
|
|
5
|
+
private cachedSecret;
|
|
6
|
+
private loaded;
|
|
7
|
+
private loadPromise;
|
|
8
|
+
load(): Promise<string>;
|
|
9
|
+
private readFromKeychain;
|
|
10
|
+
get(): string;
|
|
11
|
+
set(secret: string): Promise<boolean>;
|
|
12
|
+
clear(): Promise<boolean>;
|
|
13
|
+
hasSecret(): boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const _default: SettingsSecretStore;
|
|
16
|
+
export default _default;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single process-wide lock shared by the auto-sync loop, the config-change
|
|
3
|
+
* listener and every cloud adapter.
|
|
4
|
+
*
|
|
5
|
+
* Previously each adapter kept its own module-level `isSyncingInProgress`
|
|
6
|
+
* boolean and `index.ts` kept a separate `autoSynInProgress` flag. Because they
|
|
7
|
+
* were independent, a scheduled push could silently no-op while the cycle still
|
|
8
|
+
* reported success. One lock removes that class of bug.
|
|
9
|
+
*/
|
|
10
|
+
declare class SyncLock {
|
|
11
|
+
private locked;
|
|
12
|
+
private owner;
|
|
13
|
+
private acquiredAt;
|
|
14
|
+
/**
|
|
15
|
+
* Safety valve: if a lock is somehow never released (an adapter throwing
|
|
16
|
+
* outside its `try`, a hung socket) we would stop syncing forever. After
|
|
17
|
+
* this many milliseconds the lock is considered stale and can be taken.
|
|
18
|
+
*/
|
|
19
|
+
private static readonly STALE_AFTER_MS;
|
|
20
|
+
/** Whether a sync is currently running. */
|
|
21
|
+
get isLocked(): boolean;
|
|
22
|
+
/** Name of the operation currently holding the lock, if any. */
|
|
23
|
+
get currentOwner(): string;
|
|
24
|
+
/** True when the current holder has held the lock beyond the stale timeout. */
|
|
25
|
+
private isStale;
|
|
26
|
+
/**
|
|
27
|
+
* Try to take the lock.
|
|
28
|
+
*
|
|
29
|
+
* @param owner Label describing the caller, used in log output.
|
|
30
|
+
* @returns `true` when the lock was acquired, `false` when a sync is
|
|
31
|
+
* already running and the caller should back off.
|
|
32
|
+
*/
|
|
33
|
+
acquire(owner: string): boolean;
|
|
34
|
+
/** Release the lock. Safe to call when not held. */
|
|
35
|
+
release(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn` while holding the lock, releasing it even if `fn` throws.
|
|
38
|
+
*
|
|
39
|
+
* @returns The result of `fn`, or `fallback` when the lock was unavailable.
|
|
40
|
+
*/
|
|
41
|
+
runExclusive<T>(owner: string, fn: () => Promise<T>, fallback: T): Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
declare const _default: SyncLock;
|
|
44
|
+
export default _default;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface SyncPayload {
|
|
2
|
+
tabbyConfig: string;
|
|
3
|
+
sshConfig: string;
|
|
4
|
+
valid: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface SshConfigBlock {
|
|
7
|
+
key: string;
|
|
8
|
+
lines: string[];
|
|
9
|
+
}
|
|
10
|
+
/** Make Tabby SSH profile key paths portable before uploading. */
|
|
11
|
+
export declare function normaliseTabbyConfigPaths(content: string): string;
|
|
12
|
+
/** Expand portable key paths for the local operating system after pulling. */
|
|
13
|
+
export declare function expandTabbyConfigPaths(content: string): string;
|
|
14
|
+
/** Make IdentityFile/CertificateFile paths portable in OpenSSH config. */
|
|
15
|
+
export declare function normaliseSshConfigPaths(content: string): string;
|
|
16
|
+
/** Expand `~` key paths for the local operating system before writing OpenSSH config. */
|
|
17
|
+
export declare function expandSshConfigPaths(content: string): string;
|
|
18
|
+
/** Addresses and names that identify this computer rather than a remote host. */
|
|
19
|
+
export declare function getLocalHostIdentifiers(): Set<string>;
|
|
20
|
+
/** Remove cloud Host blocks that point at this computer; return them for re-uploading. */
|
|
21
|
+
export declare function excludeSelfReferencingSshConfig(cloudConfig: string, identifiers?: Set<string>): {
|
|
22
|
+
config: string;
|
|
23
|
+
excluded: SshConfigBlock[];
|
|
24
|
+
};
|
|
25
|
+
/** Put excluded cloud blocks back when uploading so other devices keep their connections. */
|
|
26
|
+
export declare function restoreExcludedSshBlocks(localConfig: string, excluded: SshConfigBlock[]): string;
|
|
27
|
+
/** Exclude machine-specific local-shell profiles from the cloud copy. */
|
|
28
|
+
export declare function stripLocalProfiles(content: string): string;
|
|
29
|
+
/** Apply cloud settings while retaining this device's own local-shell profiles. */
|
|
30
|
+
export declare function mergeLocalProfiles(localContent: string, cloudContent: string): string;
|
|
31
|
+
/** Resolve the OpenSSH config used by Tabby's built-in OpenSSH importer. */
|
|
32
|
+
export declare function getOpenSshConfigPath(): string;
|
|
33
|
+
/** Append the OpenSSH config as YAML comments, keeping legacy cloud files valid YAML. */
|
|
34
|
+
export declare function createSyncPayload(tabbyConfig: string, sshConfig?: string): string;
|
|
35
|
+
/** Separate the Tabby YAML from the optional SSH payload; old cloud YAML has no marker. */
|
|
36
|
+
export declare function splitSyncPayload(content: string): SyncPayload;
|
|
37
|
+
/** Include the SSH sidecar in the existing canonical config fingerprint. */
|
|
38
|
+
export declare function hashSyncContent(content: string): string;
|