t3code-cli 0.11.0 → 0.12.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 +42 -10
- package/dist/application.js +2 -2
- package/dist/auth.js +1 -1
- package/dist/bin.js +85526 -2662
- package/dist/cli.js +15 -2
- package/dist/config.js +120 -2
- package/dist/connection.js +2 -2
- package/dist/index.js +1 -1
- package/dist/node.js +6 -2
- package/dist/orchestration.js +2 -2
- package/dist/rpc.js +1 -1
- package/dist/runtime.js +2 -2
- package/dist/shared.js +42956 -38119
- package/dist/src/application/actions.d.ts +25 -0
- package/dist/src/application/index.d.ts +3 -3
- package/dist/src/application/layer.d.ts +51 -35
- package/dist/src/application/model-selection.d.ts +2 -2
- package/dist/src/application/models.d.ts +1 -1
- package/dist/src/application/project-commands.d.ts +22 -2
- package/dist/src/application/projects.d.ts +7 -7
- package/dist/src/application/service.d.ts +65 -2
- package/dist/src/application/shell-sequence.d.ts +11 -0
- package/dist/src/application/terminals.d.ts +2 -2
- package/dist/src/application/thread-commands.d.ts +1 -1
- package/dist/src/application/thread-wait.d.ts +9 -0
- package/dist/src/application/threads.d.ts +56 -5
- package/dist/src/auth/error.d.ts +2 -1
- package/dist/src/auth/index.d.ts +1 -1
- package/dist/src/auth/layer.d.ts +34 -6
- package/dist/src/auth/local-base-dir.d.ts +5 -2
- package/dist/src/auth/local-origin.d.ts +3 -3
- package/dist/src/auth/local-token.d.ts +3 -3
- package/dist/src/auth/service.d.ts +15 -3
- package/dist/src/auth/type.d.ts +40 -1
- package/dist/src/cli/env/flag.d.ts +2 -0
- package/dist/src/cli/env/index.d.ts +2 -0
- package/dist/src/cli/env/selection-layer.d.ts +3 -0
- package/dist/src/cli/flags.d.ts +2 -0
- package/dist/src/cli/format/index.d.ts +1 -0
- package/dist/src/cli/format/output.d.ts +12 -0
- package/dist/src/cli/index.d.ts +4 -0
- package/dist/src/cli/runtime/index.d.ts +1 -0
- package/dist/src/cli/runtime/service.d.ts +16 -0
- package/dist/src/{scope → cli/scope}/resolve.d.ts +5 -4
- package/dist/src/config/config.d.ts +49 -0
- package/dist/src/config/credential/cipher-node.d.ts +3 -0
- package/dist/src/config/credential/cipher-web.d.ts +3 -0
- package/dist/src/config/credential/cipher.d.ts +29 -0
- package/dist/src/config/credential/env.d.ts +3 -0
- package/dist/src/config/credential/error.d.ts +8 -0
- package/dist/src/config/credential/index.d.ts +6 -0
- package/dist/src/config/credential/service.d.ts +34 -0
- package/dist/src/config/env/env.d.ts +55 -0
- package/dist/src/config/env/index.d.ts +2 -0
- package/dist/src/{layout/base-dir.d.ts → config/env/layout.d.ts} +0 -1
- package/dist/src/config/environment-name/index.d.ts +1 -0
- package/dist/src/config/environment-name/name.d.ts +10 -0
- package/dist/src/config/error.d.ts +4 -11
- package/dist/src/config/index.d.ts +10 -5
- package/dist/src/config/keystore/error.d.ts +8 -0
- package/dist/src/config/keystore/file.d.ts +9 -0
- package/dist/src/config/keystore/index.d.ts +4 -0
- package/dist/src/config/keystore/keyring-node.d.ts +4 -0
- package/dist/src/config/keystore/service.d.ts +33 -0
- package/dist/src/config/paths/index.d.ts +1 -0
- package/dist/src/config/paths/paths.d.ts +6 -0
- package/dist/src/config/persist/file-mode.d.ts +4 -0
- package/dist/src/config/persist/migration.d.ts +68 -0
- package/dist/src/config/persist/persist.d.ts +40 -0
- package/dist/src/config/persist/schema.d.ts +68 -0
- package/dist/src/config/resolve/resolve.d.ts +40 -0
- package/dist/src/config/selection/index.d.ts +1 -0
- package/dist/src/config/selection/resolve.d.ts +4 -0
- package/dist/src/config/selection/service.d.ts +10 -0
- package/dist/src/config/types.d.ts +36 -0
- package/dist/src/config/url/error.d.ts +10 -0
- package/dist/src/config/url/index.d.ts +2 -0
- package/dist/src/connection/index.d.ts +0 -1
- package/dist/src/contracts/index.d.ts +1 -1
- package/dist/src/domain/error.d.ts +14 -1
- package/dist/src/domain/helpers.d.ts +16 -1
- package/dist/src/domain/model-config.d.ts +4 -1
- package/dist/src/domain/thread-activities.d.ts +1 -1
- package/dist/src/domain/thread-lifecycle.d.ts +10 -1
- package/dist/src/index.d.ts +1 -2
- package/dist/src/{connection/node.d.ts → node/connection.d.ts} +1 -1
- package/dist/src/node/index.d.ts +1 -2
- package/dist/src/orchestration/index.d.ts +0 -1
- package/dist/src/orchestration/layer.d.ts +153 -7
- package/dist/src/orchestration/service.d.ts +1 -1
- package/dist/src/rpc/error.d.ts +2 -2
- package/dist/src/rpc/index.d.ts +2 -3
- package/dist/src/rpc/layer.d.ts +574 -46
- package/dist/src/rpc/operation.d.ts +574 -46
- package/dist/src/rpc/ws-group.d.ts +184 -19
- package/dist/src/runtime/index.d.ts +1 -2
- package/dist/src/runtime/layer.d.ts +12 -8
- package/dist/src/t3tools/index.d.ts +1 -1
- package/dist/t3tools.js +2 -2
- package/package.json +25 -21
- package/src/application/actions.test.ts +328 -0
- package/src/application/actions.ts +475 -0
- package/src/application/index.ts +9 -5
- package/src/application/layer.ts +9 -0
- package/src/application/model-selection.ts +2 -2
- package/src/application/project-commands.ts +23 -5
- package/src/application/projects.ts +6 -7
- package/src/application/service.ts +79 -1
- package/src/application/shell-sequence.ts +1 -1
- package/src/application/terminals.ts +2 -1
- package/src/application/thread-commands.ts +1 -1
- package/src/application/thread-update.ts +1 -1
- package/src/application/thread-wait.ts +1 -1
- package/src/application/threads.test.ts +9 -3
- package/src/application/threads.ts +6 -6
- package/src/auth/error.ts +2 -1
- package/src/auth/index.ts +7 -0
- package/src/auth/layer.test-utils.ts +34 -0
- package/src/auth/layer.ts +116 -34
- package/src/auth/local-base-dir.ts +13 -6
- package/src/auth/local-origin.ts +10 -7
- package/src/auth/local-token.ts +28 -23
- package/src/auth/service.test.ts +200 -0
- package/src/auth/service.ts +27 -3
- package/src/auth/transport.ts +2 -2
- package/src/auth/type.ts +47 -1
- package/src/bin.ts +24 -6
- package/src/cli/action-format.ts +110 -0
- package/src/cli/action.ts +398 -0
- package/src/cli/app.ts +6 -0
- package/src/cli/auth.ts +76 -23
- package/src/cli/env/flag.ts +8 -0
- package/src/cli/env/index.ts +2 -0
- package/src/cli/env/selection-layer.test.ts +49 -0
- package/src/cli/env/selection-layer.ts +27 -0
- package/src/cli/env.ts +107 -0
- package/src/cli/extra-args.test.ts +32 -0
- package/src/cli/extra-args.ts +22 -0
- package/src/cli/flags.ts +11 -1
- package/src/cli/format/auth.ts +92 -0
- package/src/cli/format/human.ts +131 -0
- package/src/cli/format/index.ts +12 -0
- package/src/cli/format/model.ts +29 -0
- package/src/cli/format/output.ts +51 -0
- package/src/cli/format/project.ts +32 -0
- package/src/cli/format/terminal.ts +87 -0
- package/src/cli/{thread-format.test.ts → format/thread.test.ts} +6 -4
- package/src/cli/format/thread.ts +185 -0
- package/src/cli/index.ts +20 -0
- package/src/cli/interaction/confirm.ts +64 -0
- package/src/cli/{self-action.ts → interaction/self-action.ts} +8 -6
- package/src/cli/message-input.ts +1 -1
- package/src/cli/model.ts +9 -5
- package/src/cli/project.ts +13 -7
- package/src/cli/projects/delete.ts +13 -12
- package/src/cli/require.ts +9 -7
- package/src/cli/runtime/index.ts +1 -0
- package/src/cli/runtime/service.ts +28 -0
- package/src/{scope → cli/scope}/resolve.ts +11 -9
- package/src/cli/terminal/attach.ts +3 -6
- package/src/cli/terminal/create.ts +10 -9
- package/src/cli/terminal/destroy.ts +13 -11
- package/src/cli/terminal/list.ts +10 -9
- package/src/cli/terminal/read.test-utils.ts +48 -0
- package/src/cli/terminal/read.test.ts +35 -0
- package/src/cli/terminal/read.ts +3 -6
- package/src/cli/terminal/scope.ts +10 -5
- package/src/cli/terminal/shared.ts +1 -1
- package/src/cli/terminal/stream.ts +3 -6
- package/src/cli/terminal/wait.test.ts +5 -3
- package/src/cli/terminal/wait.ts +10 -9
- package/src/cli/terminal/write.test.ts +34 -0
- package/src/cli/terminal/write.ts +10 -9
- package/src/cli/threads/approve.ts +11 -7
- package/src/cli/threads/archive.ts +14 -9
- package/src/cli/threads/callback.ts +6 -4
- package/src/cli/threads/delete.ts +17 -11
- package/src/cli/threads/interrupt.ts +14 -9
- package/src/cli/threads/list.test.ts +5 -3
- package/src/cli/threads/list.ts +10 -10
- package/src/cli/threads/messages.ts +11 -7
- package/src/cli/threads/respond.ts +11 -7
- package/src/cli/threads/send.ts +19 -9
- package/src/cli/threads/show.ts +11 -7
- package/src/cli/threads/start.ts +18 -13
- package/src/cli/threads/unarchive.ts +11 -7
- package/src/cli/threads/update.ts +14 -9
- package/src/cli/threads/wait.ts +11 -7
- package/src/cli/wait-events.ts +1 -1
- package/src/config/config.ts +260 -0
- package/src/config/credential/cipher-node.ts +40 -0
- package/src/config/credential/cipher-web.ts +82 -0
- package/src/config/credential/cipher.ts +39 -0
- package/src/config/credential/env.ts +13 -0
- package/src/config/credential/error.ts +9 -0
- package/src/config/credential/index.ts +6 -0
- package/src/config/credential/service.test-utils.ts +25 -0
- package/src/config/credential/service.test.ts +116 -0
- package/src/config/credential/service.ts +205 -0
- package/src/config/env/env.test-utils.ts +14 -0
- package/src/config/env/env.ts +81 -0
- package/src/config/env/index.ts +2 -0
- package/src/{layout/base-dir.ts → config/env/layout.ts} +0 -9
- package/src/config/environment-name/index.ts +7 -0
- package/src/config/environment-name/name.test.ts +66 -0
- package/src/config/environment-name/name.ts +49 -0
- package/src/config/error.ts +6 -8
- package/src/config/index.ts +16 -5
- package/src/config/keystore/error.ts +9 -0
- package/src/config/keystore/file.ts +96 -0
- package/src/config/keystore/index.ts +4 -0
- package/src/config/keystore/keyring-node.test.ts +12 -0
- package/src/config/keystore/keyring-node.ts +100 -0
- package/src/config/keystore/service.test-utils.ts +15 -0
- package/src/config/keystore/service.test.ts +18 -0
- package/src/config/keystore/service.ts +29 -0
- package/src/config/layer.test-utils.ts +45 -0
- package/src/config/layer.test.ts +209 -0
- package/src/config/paths/index.ts +1 -0
- package/src/config/paths/paths.ts +36 -0
- package/src/config/persist/file-mode.ts +21 -0
- package/src/config/persist/migration.test.ts +71 -0
- package/src/config/persist/migration.ts +82 -0
- package/src/config/persist/persist.test.ts +113 -0
- package/src/config/persist/persist.ts +65 -0
- package/src/config/persist/schema.ts +37 -0
- package/src/config/platform.test-utils.ts +20 -0
- package/src/config/resolve/resolve.test-utils.ts +20 -0
- package/src/config/resolve/resolve.test.ts +137 -0
- package/src/config/resolve/resolve.ts +104 -0
- package/src/config/selection/index.ts +1 -0
- package/src/config/selection/resolve.ts +14 -0
- package/src/config/selection/service.ts +29 -0
- package/src/config/temp-home.test-utils.ts +7 -0
- package/src/config/types.ts +49 -0
- package/src/config/url/error.ts +8 -0
- package/src/config/url/index.ts +7 -0
- package/src/config/url/url.test.ts +31 -0
- package/src/connection/index.ts +0 -1
- package/src/contracts/index.ts +1 -1
- package/src/domain/error.ts +20 -1
- package/src/domain/helpers.test.ts +1 -1
- package/src/domain/helpers.ts +1 -1
- package/src/domain/model-config.ts +1 -1
- package/src/domain/thread-activities.test.ts +1 -1
- package/src/domain/thread-activities.ts +1 -1
- package/src/domain/thread-lifecycle.test.ts +1 -1
- package/src/domain/thread-lifecycle.ts +1 -1
- package/src/effect.test-utils.ts +25 -0
- package/src/index.ts +1 -1
- package/src/{connection/node.ts → node/connection.ts} +1 -1
- package/src/node/index.ts +1 -2
- package/src/orchestration/index.ts +0 -1
- package/src/orchestration/layer.ts +1 -1
- package/src/orchestration/service.ts +1 -1
- package/src/rpc/error.ts +2 -2
- package/src/rpc/index.ts +2 -3
- package/src/rpc/layer.ts +1 -1
- package/src/rpc/ws-group.ts +1 -1
- package/src/runtime/index.ts +2 -1
- package/src/runtime/layer.test-utils.ts +32 -0
- package/src/runtime/layer.test.ts +66 -0
- package/src/runtime/layer.ts +15 -9
- package/src/t3tools/index.ts +1 -1
- package/src/types/marked-terminal.d.ts +11 -0
- package/dist/layout.js +0 -2
- package/dist/scope.js +0 -2
- package/dist/src/cli/output-format.d.ts +0 -11
- package/dist/src/config/layer.d.ts +0 -22
- package/dist/src/config/service.d.ts +0 -22
- package/dist/src/environment/layer.d.ts +0 -3
- package/dist/src/environment/service.d.ts +0 -12
- package/dist/src/layout/index.d.ts +0 -1
- package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +0 -441
- package/dist/upstream-t3code/packages/contracts/src/baseSchemas.d.ts +0 -38
- package/dist/upstream-t3code/packages/contracts/src/desktopBootstrap.d.ts +0 -14
- package/dist/upstream-t3code/packages/contracts/src/editor.d.ts +0 -124
- package/dist/upstream-t3code/packages/contracts/src/environment.d.ts +0 -64
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +0 -762
- package/dist/upstream-t3code/packages/contracts/src/filesystem.d.ts +0 -26
- package/dist/upstream-t3code/packages/contracts/src/git.d.ts +0 -500
- package/dist/upstream-t3code/packages/contracts/src/index.d.ts +0 -25
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -490
- package/dist/upstream-t3code/packages/contracts/src/keybindings.d.ts +0 -133
- package/dist/upstream-t3code/packages/contracts/src/model.d.ts +0 -112
- package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +0 -6682
- package/dist/upstream-t3code/packages/contracts/src/project.d.ts +0 -45
- package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -116
- package/dist/upstream-t3code/packages/contracts/src/providerInstance.d.ts +0 -99
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +0 -4276
- package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +0 -1262
- package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +0 -48
- package/dist/upstream-t3code/packages/contracts/src/remoteAccess.d.ts +0 -45
- package/dist/upstream-t3code/packages/contracts/src/review.d.ts +0 -37
- package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -8218
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -2291
- package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -271
- package/dist/upstream-t3code/packages/contracts/src/sourceControl.d.ts +0 -177
- package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +0 -330
- package/dist/upstream-t3code/packages/contracts/src/vcs.d.ts +0 -148
- package/src/cli/auth-format.ts +0 -45
- package/src/cli/confirm.ts +0 -31
- package/src/cli/model-format.ts +0 -18
- package/src/cli/output-format.ts +0 -45
- package/src/cli/project-format.ts +0 -18
- package/src/cli/terminal/commands.test.ts +0 -101
- package/src/cli/terminal-format.ts +0 -167
- package/src/cli/thread-format.ts +0 -128
- package/src/config/layer.ts +0 -105
- package/src/config/service.ts +0 -26
- package/src/environment/layer.ts +0 -12
- package/src/environment/service.ts +0 -13
- package/src/layout/index.ts +0 -1
- /package/dist/src/{scope → cli/scope}/index.d.ts +0 -0
- /package/dist/src/config/{url.d.ts → url/url.d.ts} +0 -0
- /package/src/{scope → cli/scope}/index.ts +0 -0
- /package/src/config/{url.ts → url/url.ts} +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Path from "effect/Path";
|
|
8
|
+
|
|
9
|
+
import { credentialTokenFromEnv } from "./credential/env.ts";
|
|
10
|
+
import { T3CredentialCrypto } from "./credential/service.ts";
|
|
11
|
+
import { loadT3CliEnv } from "./env/env.ts";
|
|
12
|
+
import { validateEnvironmentName } from "./environment-name/name.ts";
|
|
13
|
+
import { ConfigError } from "./error.ts";
|
|
14
|
+
import { readEncryptedConfigFile, writeEncryptedConfigFile } from "./persist/persist.ts";
|
|
15
|
+
import {
|
|
16
|
+
buildResolvedConfigFromEnv,
|
|
17
|
+
buildResolvedConfigFromStored,
|
|
18
|
+
resolveDefaultForUpsert,
|
|
19
|
+
selectEnvironmentName,
|
|
20
|
+
summarizeEnvironments,
|
|
21
|
+
validateCredentialEnvVars,
|
|
22
|
+
} from "./resolve/resolve.ts";
|
|
23
|
+
import { T3ConfigSelection } from "./selection/service.ts";
|
|
24
|
+
import type { EnvironmentSummary, ResolvedConfig, UpsertEnvironmentInput } from "./types.ts";
|
|
25
|
+
import { normalizeHttpBaseUrl } from "./url/url.ts";
|
|
26
|
+
|
|
27
|
+
export class T3Config extends Context.Service<
|
|
28
|
+
T3Config,
|
|
29
|
+
{
|
|
30
|
+
readonly resolve: () => Effect.Effect<ResolvedConfig, ConfigError>;
|
|
31
|
+
readonly resolveActiveEnvironmentName: () => Effect.Effect<string | undefined, ConfigError>;
|
|
32
|
+
readonly listEnvironments: () => Effect.Effect<readonly EnvironmentSummary[], ConfigError>;
|
|
33
|
+
readonly upsertEnvironment: (input: UpsertEnvironmentInput) => Effect.Effect<void, ConfigError>;
|
|
34
|
+
readonly setDefaultEnvironment: (name: string) => Effect.Effect<void, ConfigError>;
|
|
35
|
+
readonly removeEnvironment: (name: string) => Effect.Effect<void, ConfigError>;
|
|
36
|
+
readonly hasEnvironment: (name: string) => Effect.Effect<boolean, ConfigError>;
|
|
37
|
+
readonly getDefaultEnvironmentName: () => Effect.Effect<string | undefined, ConfigError>;
|
|
38
|
+
}
|
|
39
|
+
>()("t3cli/T3Config") {}
|
|
40
|
+
|
|
41
|
+
export const make = Effect.fn("makeT3Config")(function* () {
|
|
42
|
+
const configSelection = yield* T3ConfigSelection;
|
|
43
|
+
const credentialCrypto = yield* T3CredentialCrypto;
|
|
44
|
+
const t3CliEnv = yield* loadT3CliEnv;
|
|
45
|
+
const fs = yield* FileSystem.FileSystem;
|
|
46
|
+
const path = yield* Path.Path;
|
|
47
|
+
const configProvider = yield* ConfigProvider.ConfigProvider;
|
|
48
|
+
const services = Layer.mergeAll(
|
|
49
|
+
Layer.succeed(FileSystem.FileSystem, fs),
|
|
50
|
+
Layer.succeed(Path.Path, path),
|
|
51
|
+
Layer.succeed(T3CredentialCrypto, credentialCrypto),
|
|
52
|
+
Layer.succeed(ConfigProvider.ConfigProvider, configProvider),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const hasEnvironment = Effect.fn("T3Config.hasEnvironment")(function* (name: string) {
|
|
56
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
57
|
+
return encrypted.environments[name] !== undefined;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const getDefaultEnvironmentName = Effect.fn("T3Config.getDefaultEnvironmentName")(function* () {
|
|
61
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
62
|
+
const defaultName = encrypted.default;
|
|
63
|
+
return defaultName !== undefined && defaultName.length > 0 ? defaultName : undefined;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const resolveActiveEnvironmentName = Effect.fn("T3Config.resolveActiveEnvironmentName")(
|
|
67
|
+
function* () {
|
|
68
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
69
|
+
const envUrl = Option.getOrUndefined(t3CliEnv.t3codeUrl);
|
|
70
|
+
const envToken = Option.getOrUndefined(credentialTokenFromEnv(t3CliEnv));
|
|
71
|
+
yield* validateCredentialEnvVars({ envUrl, envToken });
|
|
72
|
+
const hasEnvCredentials = envUrl !== undefined && envToken !== undefined;
|
|
73
|
+
if (hasEnvCredentials) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const configuredEnvironment = yield* configSelection.getSelectedEnvironment();
|
|
77
|
+
const selectedName = selectEnvironmentName({
|
|
78
|
+
selectedEnvironment: configuredEnvironment,
|
|
79
|
+
defaultEnvironment: encrypted.default,
|
|
80
|
+
});
|
|
81
|
+
if (selectedName === undefined || selectedName.length === 0) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
if (encrypted.environments[selectedName] === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return selectedName;
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const listEnvironments = Effect.fn("T3Config.listEnvironments")(function* () {
|
|
92
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
93
|
+
return summarizeEnvironments(encrypted);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const upsertEnvironment = Effect.fn("T3Config.upsertEnvironment")(function* (
|
|
97
|
+
input: UpsertEnvironmentInput,
|
|
98
|
+
) {
|
|
99
|
+
yield* validateEnvironmentName(input.name);
|
|
100
|
+
const normalizedUrl = yield* normalizeHttpBaseUrl(input.url).pipe(
|
|
101
|
+
Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
|
|
102
|
+
);
|
|
103
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
104
|
+
const token = yield* credentialCrypto.encrypt({
|
|
105
|
+
environmentName: input.name,
|
|
106
|
+
url: normalizedUrl,
|
|
107
|
+
local: input.local,
|
|
108
|
+
token: input.token,
|
|
109
|
+
});
|
|
110
|
+
const defaultName = resolveDefaultForUpsert(encrypted, input.name, input.makeDefault);
|
|
111
|
+
yield* Effect.provide(
|
|
112
|
+
writeEncryptedConfigFile({
|
|
113
|
+
version: 2,
|
|
114
|
+
...(defaultName !== undefined ? { default: defaultName } : {}),
|
|
115
|
+
environments: {
|
|
116
|
+
...encrypted.environments,
|
|
117
|
+
[input.name]: {
|
|
118
|
+
url: normalizedUrl,
|
|
119
|
+
local: input.local,
|
|
120
|
+
token,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
services,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const setDefaultEnvironment = Effect.fn("T3Config.setDefaultEnvironment")(function* (
|
|
129
|
+
name: string,
|
|
130
|
+
) {
|
|
131
|
+
yield* validateEnvironmentName(name);
|
|
132
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
133
|
+
if (encrypted.environments[name] === undefined) {
|
|
134
|
+
return yield* Effect.fail(new ConfigError({ message: `environment not found: ${name}` }));
|
|
135
|
+
}
|
|
136
|
+
const selectedEnvironment = encrypted.environments[name];
|
|
137
|
+
yield* credentialCrypto
|
|
138
|
+
.decrypt({
|
|
139
|
+
environmentName: name,
|
|
140
|
+
url: selectedEnvironment.url,
|
|
141
|
+
local: selectedEnvironment.local,
|
|
142
|
+
token: selectedEnvironment.token,
|
|
143
|
+
})
|
|
144
|
+
.pipe(
|
|
145
|
+
Effect.mapError(
|
|
146
|
+
(error) =>
|
|
147
|
+
new ConfigError({
|
|
148
|
+
message: `failed to decrypt credentials for environment '${name}'`,
|
|
149
|
+
cause: error.cause,
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
Effect.asVoid,
|
|
153
|
+
);
|
|
154
|
+
yield* Effect.provide(
|
|
155
|
+
writeEncryptedConfigFile({
|
|
156
|
+
...encrypted,
|
|
157
|
+
default: name,
|
|
158
|
+
}),
|
|
159
|
+
services,
|
|
160
|
+
);
|
|
161
|
+
return yield* Effect.void;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const removeEnvironment = Effect.fn("T3Config.removeEnvironment")(function* (name: string) {
|
|
165
|
+
yield* validateEnvironmentName(name);
|
|
166
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
167
|
+
if (encrypted.environments[name] === undefined) {
|
|
168
|
+
return yield* Effect.fail(new ConfigError({ message: `environment not found: ${name}` }));
|
|
169
|
+
}
|
|
170
|
+
const { [name]: _removed, ...environments } = encrypted.environments;
|
|
171
|
+
const defaultName = encrypted.default === name ? undefined : encrypted.default;
|
|
172
|
+
yield* Effect.provide(
|
|
173
|
+
writeEncryptedConfigFile({
|
|
174
|
+
version: 2,
|
|
175
|
+
...(defaultName !== undefined ? { default: defaultName } : {}),
|
|
176
|
+
environments,
|
|
177
|
+
}),
|
|
178
|
+
services,
|
|
179
|
+
);
|
|
180
|
+
return yield* Effect.void;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const resolve = Effect.fn("T3Config.resolve")(function* () {
|
|
184
|
+
const encrypted = yield* Effect.provide(readEncryptedConfigFile(), services);
|
|
185
|
+
const envUrl = Option.getOrUndefined(t3CliEnv.t3codeUrl);
|
|
186
|
+
const envToken = Option.getOrUndefined(credentialTokenFromEnv(t3CliEnv));
|
|
187
|
+
yield* validateCredentialEnvVars({ envUrl, envToken });
|
|
188
|
+
const hasEnvUrl = envUrl !== undefined;
|
|
189
|
+
const configuredEnvironment = yield* configSelection.getSelectedEnvironment();
|
|
190
|
+
const selectedName = selectEnvironmentName({
|
|
191
|
+
selectedEnvironment: configuredEnvironment,
|
|
192
|
+
defaultEnvironment: encrypted.default,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (hasEnvUrl && envToken !== undefined) {
|
|
196
|
+
if (selectedName !== undefined && selectedName.length > 0) {
|
|
197
|
+
yield* validateEnvironmentName(selectedName);
|
|
198
|
+
if (encrypted.environments[selectedName] === undefined) {
|
|
199
|
+
return yield* Effect.fail(
|
|
200
|
+
new ConfigError({ message: `environment not found: ${selectedName}` }),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return yield* buildResolvedConfigFromEnv({
|
|
205
|
+
envUrl,
|
|
206
|
+
envToken,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (selectedName === undefined || selectedName.length === 0) {
|
|
211
|
+
return yield* Effect.fail(
|
|
212
|
+
new ConfigError({
|
|
213
|
+
message: "not authenticated. run: t3cli auth pair --url <pairing-url>",
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
yield* validateEnvironmentName(selectedName);
|
|
218
|
+
if (encrypted.environments[selectedName] === undefined) {
|
|
219
|
+
return yield* Effect.fail(
|
|
220
|
+
new ConfigError({ message: `environment not found: ${selectedName}` }),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const selectedEnvironment = encrypted.environments[selectedName];
|
|
225
|
+
const token = yield* credentialCrypto
|
|
226
|
+
.decrypt({
|
|
227
|
+
environmentName: selectedName,
|
|
228
|
+
url: selectedEnvironment.url,
|
|
229
|
+
local: selectedEnvironment.local,
|
|
230
|
+
token: selectedEnvironment.token,
|
|
231
|
+
})
|
|
232
|
+
.pipe(
|
|
233
|
+
Effect.mapError(
|
|
234
|
+
(error) =>
|
|
235
|
+
new ConfigError({
|
|
236
|
+
message: `failed to decrypt credentials for environment '${selectedName}'`,
|
|
237
|
+
cause: error.cause,
|
|
238
|
+
}),
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
return yield* buildResolvedConfigFromStored({
|
|
242
|
+
selectedName,
|
|
243
|
+
token,
|
|
244
|
+
encrypted,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
resolve,
|
|
250
|
+
resolveActiveEnvironmentName,
|
|
251
|
+
listEnvironments,
|
|
252
|
+
upsertEnvironment,
|
|
253
|
+
setDefaultEnvironment,
|
|
254
|
+
removeEnvironment,
|
|
255
|
+
hasEnvironment,
|
|
256
|
+
getDefaultEnvironmentName,
|
|
257
|
+
} as const;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
export const layer = Layer.effect(T3Config, make());
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
|
|
6
|
+
import { credentialCipherTagByteLength, T3CredentialCipher } from "./cipher.ts";
|
|
7
|
+
import { CredentialCipherError } from "./error.ts";
|
|
8
|
+
|
|
9
|
+
const make = (): T3CredentialCipher["Service"] => ({
|
|
10
|
+
encrypt: (input) =>
|
|
11
|
+
Effect.try({
|
|
12
|
+
try: () => {
|
|
13
|
+
const cipher = createCipheriv("aes-256-gcm", input.key, input.nonce, {
|
|
14
|
+
authTagLength: credentialCipherTagByteLength,
|
|
15
|
+
});
|
|
16
|
+
cipher.setAAD(input.additionalData);
|
|
17
|
+
const ciphertext = Buffer.concat([cipher.update(input.plaintext), cipher.final()]);
|
|
18
|
+
return {
|
|
19
|
+
ciphertext: new Uint8Array(ciphertext),
|
|
20
|
+
tag: new Uint8Array(cipher.getAuthTag()),
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
catch: (cause) => new CredentialCipherError({ operation: "encrypt", cause }),
|
|
24
|
+
}),
|
|
25
|
+
decrypt: (input) =>
|
|
26
|
+
Effect.try({
|
|
27
|
+
try: () => {
|
|
28
|
+
const decipher = createDecipheriv("aes-256-gcm", input.key, input.nonce, {
|
|
29
|
+
authTagLength: credentialCipherTagByteLength,
|
|
30
|
+
});
|
|
31
|
+
decipher.setAAD(input.additionalData);
|
|
32
|
+
decipher.setAuthTag(input.tag);
|
|
33
|
+
const plaintext = Buffer.concat([decipher.update(input.ciphertext), decipher.final()]);
|
|
34
|
+
return new Uint8Array(plaintext);
|
|
35
|
+
},
|
|
36
|
+
catch: (cause) => new CredentialCipherError({ operation: "decrypt", cause }),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const layerNode = Layer.succeed(T3CredentialCipher, make());
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
|
|
4
|
+
import { credentialCipherTagByteLength, T3CredentialCipher } from "./cipher.ts";
|
|
5
|
+
import { CredentialCipherError } from "./error.ts";
|
|
6
|
+
|
|
7
|
+
const aesGcmAlgorithm = "AES-GCM";
|
|
8
|
+
|
|
9
|
+
function copyBytes(bytes: Uint8Array) {
|
|
10
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
11
|
+
copy.set(bytes);
|
|
12
|
+
return copy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function importAesGcmKey(key: Uint8Array, operation: "encrypt" | "decrypt") {
|
|
16
|
+
return Effect.tryPromise({
|
|
17
|
+
try: () =>
|
|
18
|
+
globalThis.crypto.subtle.importKey("raw", copyBytes(key), { name: aesGcmAlgorithm }, false, [
|
|
19
|
+
"encrypt",
|
|
20
|
+
"decrypt",
|
|
21
|
+
]),
|
|
22
|
+
catch: (cause) => new CredentialCipherError({ operation, cause }),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const make = (): T3CredentialCipher["Service"] => ({
|
|
27
|
+
encrypt: (input) =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const cryptoKey = yield* importAesGcmKey(input.key, "encrypt");
|
|
30
|
+
const encrypted = yield* Effect.tryPromise({
|
|
31
|
+
try: () =>
|
|
32
|
+
globalThis.crypto.subtle.encrypt(
|
|
33
|
+
{
|
|
34
|
+
name: aesGcmAlgorithm,
|
|
35
|
+
iv: copyBytes(input.nonce),
|
|
36
|
+
additionalData: copyBytes(input.additionalData),
|
|
37
|
+
tagLength: credentialCipherTagByteLength * 8,
|
|
38
|
+
},
|
|
39
|
+
cryptoKey,
|
|
40
|
+
copyBytes(input.plaintext),
|
|
41
|
+
),
|
|
42
|
+
catch: (cause) => new CredentialCipherError({ operation: "encrypt", cause }),
|
|
43
|
+
});
|
|
44
|
+
const bytes = new Uint8Array(encrypted);
|
|
45
|
+
if (bytes.byteLength < credentialCipherTagByteLength) {
|
|
46
|
+
return yield* Effect.fail(
|
|
47
|
+
new CredentialCipherError({
|
|
48
|
+
operation: "encrypt",
|
|
49
|
+
cause: new Error("encrypted output shorter than auth tag"),
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ciphertext: bytes.slice(0, -credentialCipherTagByteLength),
|
|
55
|
+
tag: bytes.slice(-credentialCipherTagByteLength),
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
decrypt: (input) =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
const cryptoKey = yield* importAesGcmKey(input.key, "decrypt");
|
|
61
|
+
const combined = new Uint8Array(input.ciphertext.byteLength + input.tag.byteLength);
|
|
62
|
+
combined.set(input.ciphertext);
|
|
63
|
+
combined.set(input.tag, input.ciphertext.byteLength);
|
|
64
|
+
const plaintext = yield* Effect.tryPromise({
|
|
65
|
+
try: () =>
|
|
66
|
+
globalThis.crypto.subtle.decrypt(
|
|
67
|
+
{
|
|
68
|
+
name: aesGcmAlgorithm,
|
|
69
|
+
iv: copyBytes(input.nonce),
|
|
70
|
+
additionalData: copyBytes(input.additionalData),
|
|
71
|
+
tagLength: credentialCipherTagByteLength * 8,
|
|
72
|
+
},
|
|
73
|
+
cryptoKey,
|
|
74
|
+
combined,
|
|
75
|
+
),
|
|
76
|
+
catch: (cause) => new CredentialCipherError({ operation: "decrypt", cause }),
|
|
77
|
+
});
|
|
78
|
+
return new Uint8Array(plaintext);
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const layerWeb = Layer.succeed(T3CredentialCipher, make());
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
|
|
4
|
+
import type { CredentialCipherError } from "./error.ts";
|
|
5
|
+
|
|
6
|
+
export const credentialCipherNonceByteLength = 12;
|
|
7
|
+
export const credentialCipherTagByteLength = 16;
|
|
8
|
+
|
|
9
|
+
export type AesGcmEncryptInput = {
|
|
10
|
+
readonly key: Uint8Array;
|
|
11
|
+
readonly nonce: Uint8Array;
|
|
12
|
+
readonly plaintext: Uint8Array;
|
|
13
|
+
readonly additionalData: Uint8Array;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AesGcmDecryptInput = {
|
|
17
|
+
readonly key: Uint8Array;
|
|
18
|
+
readonly nonce: Uint8Array;
|
|
19
|
+
readonly ciphertext: Uint8Array;
|
|
20
|
+
readonly tag: Uint8Array;
|
|
21
|
+
readonly additionalData: Uint8Array;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AesGcmEncryptResult = {
|
|
25
|
+
readonly ciphertext: Uint8Array;
|
|
26
|
+
readonly tag: Uint8Array;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class T3CredentialCipher extends Context.Service<
|
|
30
|
+
T3CredentialCipher,
|
|
31
|
+
{
|
|
32
|
+
readonly encrypt: (
|
|
33
|
+
input: AesGcmEncryptInput,
|
|
34
|
+
) => Effect.Effect<AesGcmEncryptResult, CredentialCipherError>;
|
|
35
|
+
readonly decrypt: (
|
|
36
|
+
input: AesGcmDecryptInput,
|
|
37
|
+
) => Effect.Effect<Uint8Array, CredentialCipherError>;
|
|
38
|
+
}
|
|
39
|
+
>()("t3cli/T3CredentialCipher") {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Option from "effect/Option";
|
|
2
|
+
import * as Redacted from "effect/Redacted";
|
|
3
|
+
|
|
4
|
+
import type { T3CliEnvShape } from "../env/env.ts";
|
|
5
|
+
|
|
6
|
+
export function credentialTokenFromEnv(
|
|
7
|
+
t3CliEnv: Pick<T3CliEnvShape, "t3codeToken">,
|
|
8
|
+
): Option.Option<string> {
|
|
9
|
+
return Option.flatMap(t3CliEnv.t3codeToken, (token) => {
|
|
10
|
+
const value = Redacted.value(token).trim();
|
|
11
|
+
return value.length > 0 ? Option.some(value) : Option.none();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * as Service from "./service.ts";
|
|
2
|
+
export * as Cipher from "./cipher.ts";
|
|
3
|
+
export * as CipherNode from "./cipher-node.ts";
|
|
4
|
+
export * as CipherWeb from "./cipher-web.ts";
|
|
5
|
+
export { credentialTokenFromEnv } from "./env.ts";
|
|
6
|
+
export { CredentialCipherError } from "./error.ts";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
|
|
3
|
+
import { CliRuntime } from "../../cli/runtime/service.ts";
|
|
4
|
+
import { ConfigPlatformLayer } from "../platform.test-utils.ts";
|
|
5
|
+
import { t3CliEnvConfigLayer } from "../env/env.test-utils.ts";
|
|
6
|
+
import * as CredentialCipherWeb from "./cipher-web.ts";
|
|
7
|
+
import * as Keystore from "../keystore/service.test-utils.ts";
|
|
8
|
+
import * as Credential from "./service.ts";
|
|
9
|
+
|
|
10
|
+
export function t3CredentialCryptoDepsLayer(homeDir: string) {
|
|
11
|
+
return Credential.layer.pipe(
|
|
12
|
+
Layer.provide(
|
|
13
|
+
Layer.mergeAll(
|
|
14
|
+
CliRuntime.layerTest({ cwd: homeDir }),
|
|
15
|
+
t3CliEnvConfigLayer(homeDir),
|
|
16
|
+
CredentialCipherWeb.layerWeb,
|
|
17
|
+
Keystore.unavailableKeystoreFactoryLayer,
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function t3CredentialCryptoLayerTest(homeDir: string) {
|
|
24
|
+
return Layer.mergeAll(ConfigPlatformLayer, t3CredentialCryptoDepsLayer(homeDir));
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import * as Crypto from "effect/Crypto";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Exit from "effect/Exit";
|
|
6
|
+
import * as FileSystem from "effect/FileSystem";
|
|
7
|
+
import * as Path from "effect/Path";
|
|
8
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
9
|
+
|
|
10
|
+
import { ConfigPlatformLayer } from "../platform.test-utils.ts";
|
|
11
|
+
import { makeTempHomeScoped } from "../temp-home.test-utils.ts";
|
|
12
|
+
import { T3CredentialCrypto } from "./service.ts";
|
|
13
|
+
import { t3CredentialCryptoDepsLayer } from "./service.test-utils.ts";
|
|
14
|
+
|
|
15
|
+
describe("T3CredentialCrypto", () => {
|
|
16
|
+
it.effect("falls back to key file when keyring backend is unavailable", () =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const fs = yield* FileSystem.FileSystem;
|
|
19
|
+
const path = yield* Path.Path;
|
|
20
|
+
const cryptoService = yield* Crypto.Crypto;
|
|
21
|
+
const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
|
|
22
|
+
yield* Effect.gen(function* () {
|
|
23
|
+
const keyPath = path.join(homeDir, ".config", "t3cli", "key");
|
|
24
|
+
const masterKey = yield* cryptoService.randomBytes(32);
|
|
25
|
+
yield* fs.makeDirectory(path.dirname(keyPath), { recursive: true });
|
|
26
|
+
yield* fs.writeFileString(keyPath, `${Buffer.from(masterKey).toString("base64")}\n`, {
|
|
27
|
+
mode: 0o600,
|
|
28
|
+
});
|
|
29
|
+
const credentialCrypto = yield* T3CredentialCrypto;
|
|
30
|
+
const encrypted = yield* credentialCrypto.encrypt({
|
|
31
|
+
environmentName: "home",
|
|
32
|
+
url: "https://home.example",
|
|
33
|
+
local: false,
|
|
34
|
+
token: "secret-token",
|
|
35
|
+
});
|
|
36
|
+
const token = yield* credentialCrypto.decrypt({
|
|
37
|
+
environmentName: "home",
|
|
38
|
+
url: "https://home.example",
|
|
39
|
+
local: false,
|
|
40
|
+
token: encrypted,
|
|
41
|
+
});
|
|
42
|
+
assert.equal(token, "secret-token");
|
|
43
|
+
}).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
|
|
44
|
+
}).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
it.effect("hardens existing key file permissions to 0600 on use", () =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const fs = yield* FileSystem.FileSystem;
|
|
50
|
+
const path = yield* Path.Path;
|
|
51
|
+
const cryptoService = yield* Crypto.Crypto;
|
|
52
|
+
const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
|
|
53
|
+
yield* Effect.gen(function* () {
|
|
54
|
+
const keyPath = path.join(homeDir, ".config", "t3cli", "key");
|
|
55
|
+
const masterKey = yield* cryptoService.randomBytes(32);
|
|
56
|
+
yield* fs.makeDirectory(path.dirname(keyPath), { recursive: true });
|
|
57
|
+
yield* fs.writeFileString(keyPath, `${Buffer.from(masterKey).toString("base64")}\n`, {
|
|
58
|
+
mode: 0o644,
|
|
59
|
+
});
|
|
60
|
+
const credentialCrypto = yield* T3CredentialCrypto;
|
|
61
|
+
yield* credentialCrypto.encrypt({
|
|
62
|
+
environmentName: "home",
|
|
63
|
+
url: "https://home.example",
|
|
64
|
+
local: false,
|
|
65
|
+
token: "secret-token",
|
|
66
|
+
});
|
|
67
|
+
const keyStat = yield* fs.stat(keyPath);
|
|
68
|
+
assert.equal(keyStat.mode & 0o777, 0o600);
|
|
69
|
+
}).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
|
|
70
|
+
}).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
it.effect("creates key file with 0600 permissions when keyring is unavailable", () =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const fs = yield* FileSystem.FileSystem;
|
|
76
|
+
const path = yield* Path.Path;
|
|
77
|
+
const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
|
|
78
|
+
yield* Effect.gen(function* () {
|
|
79
|
+
const keyPath = path.join(homeDir, ".config", "t3cli", "key");
|
|
80
|
+
const credentialCrypto = yield* T3CredentialCrypto;
|
|
81
|
+
yield* credentialCrypto.encrypt({
|
|
82
|
+
environmentName: "home",
|
|
83
|
+
url: "https://home.example",
|
|
84
|
+
local: false,
|
|
85
|
+
token: "rotate-write",
|
|
86
|
+
});
|
|
87
|
+
const keyStat = yield* fs.stat(keyPath);
|
|
88
|
+
assert.equal(keyStat.mode & 0o777, 0o600);
|
|
89
|
+
}).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
|
|
90
|
+
}).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
it.effect("fails decrypt when ciphertext AAD does not match", () =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const homeDir = yield* makeTempHomeScoped("t3cli-credential-");
|
|
96
|
+
yield* Effect.gen(function* () {
|
|
97
|
+
const crypto = yield* T3CredentialCrypto;
|
|
98
|
+
const token = yield* crypto.encrypt({
|
|
99
|
+
environmentName: "home",
|
|
100
|
+
url: "https://home.example",
|
|
101
|
+
local: false,
|
|
102
|
+
token: "secret",
|
|
103
|
+
});
|
|
104
|
+
const result = yield* crypto
|
|
105
|
+
.decrypt({
|
|
106
|
+
environmentName: "home",
|
|
107
|
+
url: "https://tampered.example",
|
|
108
|
+
local: false,
|
|
109
|
+
token,
|
|
110
|
+
})
|
|
111
|
+
.pipe(Effect.exit);
|
|
112
|
+
assert.equal(Exit.isFailure(result), true);
|
|
113
|
+
}).pipe(Effect.provide(t3CredentialCryptoDepsLayer(homeDir)));
|
|
114
|
+
}).pipe(Effect.provide(ConfigPlatformLayer), Effect.scoped),
|
|
115
|
+
);
|
|
116
|
+
});
|