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,205 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Crypto from "effect/Crypto";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
5
|
+
import * as Encoding from "effect/Encoding";
|
|
6
|
+
import * as FileSystem from "effect/FileSystem";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import * as Path from "effect/Path";
|
|
9
|
+
|
|
10
|
+
import { T3CredentialCipher, credentialCipherNonceByteLength } from "./cipher.ts";
|
|
11
|
+
import type { EncryptedToken } from "../persist/schema.ts";
|
|
12
|
+
import { ConfigError } from "../error.ts";
|
|
13
|
+
import { CredentialCipherError } from "./error.ts";
|
|
14
|
+
import { makeFileKeystore } from "../keystore/file.ts";
|
|
15
|
+
import { T3MasterKeyKeystoreFactory, masterKeyByteLength } from "../keystore/service.ts";
|
|
16
|
+
|
|
17
|
+
export type CredentialEncryptInput = {
|
|
18
|
+
readonly environmentName: string;
|
|
19
|
+
readonly url: string;
|
|
20
|
+
readonly local: boolean;
|
|
21
|
+
readonly token: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type CredentialDecryptInput = {
|
|
25
|
+
readonly environmentName: string;
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly local: boolean;
|
|
28
|
+
readonly token: EncryptedToken;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class T3CredentialCrypto extends Context.Service<
|
|
32
|
+
T3CredentialCrypto,
|
|
33
|
+
{
|
|
34
|
+
readonly encrypt: (input: CredentialEncryptInput) => Effect.Effect<EncryptedToken, ConfigError>;
|
|
35
|
+
readonly decrypt: (input: CredentialDecryptInput) => Effect.Effect<string, ConfigError>;
|
|
36
|
+
}
|
|
37
|
+
>()("t3cli/T3CredentialCrypto") {}
|
|
38
|
+
|
|
39
|
+
const configSchemaVersion = 2;
|
|
40
|
+
const textEncoder = new TextEncoder();
|
|
41
|
+
const textDecoder = new TextDecoder();
|
|
42
|
+
|
|
43
|
+
export const make = Effect.fn("makeT3CredentialCrypto")(function* () {
|
|
44
|
+
const fs = yield* FileSystem.FileSystem;
|
|
45
|
+
const path = yield* Path.Path;
|
|
46
|
+
const configProvider = yield* ConfigProvider.ConfigProvider;
|
|
47
|
+
const cryptoService = yield* Crypto.Crypto;
|
|
48
|
+
const cipher = yield* T3CredentialCipher;
|
|
49
|
+
const keystoreFactory = yield* T3MasterKeyKeystoreFactory;
|
|
50
|
+
const services = Layer.mergeAll(
|
|
51
|
+
Layer.succeed(FileSystem.FileSystem, fs),
|
|
52
|
+
Layer.succeed(Path.Path, path),
|
|
53
|
+
Layer.succeed(Crypto.Crypto, cryptoService),
|
|
54
|
+
Layer.succeed(ConfigProvider.ConfigProvider, configProvider),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const fileKeystore = yield* Effect.provide(makeFileKeystore(), services);
|
|
58
|
+
const primaryKeystore = yield* keystoreFactory.make().pipe(
|
|
59
|
+
Effect.catchTags({
|
|
60
|
+
KeystoreUnavailableError: () => Effect.succeed(undefined),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const readMasterKey = Effect.fn("readMasterKey")(function* () {
|
|
65
|
+
if (primaryKeystore !== undefined) {
|
|
66
|
+
const primaryResult = yield* primaryKeystore.read();
|
|
67
|
+
if (primaryResult.kind === "present") {
|
|
68
|
+
return primaryResult.key;
|
|
69
|
+
}
|
|
70
|
+
if (primaryResult.kind === "corrupt") {
|
|
71
|
+
return yield* Effect.fail(
|
|
72
|
+
new ConfigError({
|
|
73
|
+
message: `corrupt credential key in OS keyring: ${primaryResult.message}`,
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fileResult = yield* fileKeystore.read();
|
|
80
|
+
if (fileResult.kind === "present") {
|
|
81
|
+
return fileResult.key;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const writeMasterKey = Effect.fn("writeMasterKey")(function* (key: Uint8Array) {
|
|
87
|
+
if (primaryKeystore !== undefined) {
|
|
88
|
+
yield* primaryKeystore.write(key).pipe(
|
|
89
|
+
Effect.catchTags({
|
|
90
|
+
KeystoreUnavailableError: () => fileKeystore.write(key),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
yield* fileKeystore.write(key);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const getMasterKey = Effect.fn("getMasterKey")(function* () {
|
|
99
|
+
const existing = yield* readMasterKey();
|
|
100
|
+
if (existing !== undefined) {
|
|
101
|
+
return existing;
|
|
102
|
+
}
|
|
103
|
+
const generated = yield* cryptoService.randomBytes(masterKeyByteLength).pipe(
|
|
104
|
+
Effect.mapError(
|
|
105
|
+
(error) =>
|
|
106
|
+
new ConfigError({
|
|
107
|
+
message: "failed to generate secure random bytes",
|
|
108
|
+
cause: error,
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
yield* writeMasterKey(generated);
|
|
113
|
+
return generated;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const encrypt: (input: CredentialEncryptInput) => Effect.Effect<EncryptedToken, ConfigError> =
|
|
117
|
+
Effect.fn("encrypt")(function* (input) {
|
|
118
|
+
const masterKey = yield* getMasterKey();
|
|
119
|
+
const nonce = yield* cryptoService.randomBytes(credentialCipherNonceByteLength).pipe(
|
|
120
|
+
Effect.mapError(
|
|
121
|
+
(error) =>
|
|
122
|
+
new ConfigError({
|
|
123
|
+
message: "failed to generate secure random bytes",
|
|
124
|
+
cause: error,
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
const encrypted = yield* cipher
|
|
129
|
+
.encrypt({
|
|
130
|
+
key: masterKey,
|
|
131
|
+
nonce,
|
|
132
|
+
plaintext: encodeUtf8(input.token),
|
|
133
|
+
additionalData: buildCredentialAad(input),
|
|
134
|
+
})
|
|
135
|
+
.pipe(Effect.mapError(mapCredentialCipherError));
|
|
136
|
+
return {
|
|
137
|
+
kind: "encrypted" as const,
|
|
138
|
+
alg: "aes-256-gcm" as const,
|
|
139
|
+
key: "default" as const,
|
|
140
|
+
nonce: Encoding.encodeBase64(nonce),
|
|
141
|
+
ciphertext: Encoding.encodeBase64(encrypted.ciphertext),
|
|
142
|
+
tag: Encoding.encodeBase64(encrypted.tag),
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const decrypt: (input: CredentialDecryptInput) => Effect.Effect<string, ConfigError> = Effect.fn(
|
|
147
|
+
"decrypt",
|
|
148
|
+
)(function* (input) {
|
|
149
|
+
const masterKey = yield* getMasterKey();
|
|
150
|
+
const nonce = yield* decodeBase64Field(input.token.nonce, "token nonce");
|
|
151
|
+
const ciphertext = yield* decodeBase64Field(input.token.ciphertext, "token ciphertext");
|
|
152
|
+
const tag = yield* decodeBase64Field(input.token.tag, "token tag");
|
|
153
|
+
const plaintext = yield* cipher
|
|
154
|
+
.decrypt({
|
|
155
|
+
key: masterKey,
|
|
156
|
+
nonce,
|
|
157
|
+
ciphertext,
|
|
158
|
+
tag,
|
|
159
|
+
additionalData: buildCredentialAad(input),
|
|
160
|
+
})
|
|
161
|
+
.pipe(Effect.mapError(mapCredentialCipherError));
|
|
162
|
+
return decodeUtf8(plaintext);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return { encrypt, decrypt } satisfies T3CredentialCrypto["Service"];
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const layer = Layer.effect(T3CredentialCrypto, make());
|
|
169
|
+
|
|
170
|
+
function buildCredentialAad(input: {
|
|
171
|
+
readonly environmentName: string;
|
|
172
|
+
readonly url: string;
|
|
173
|
+
readonly local: boolean;
|
|
174
|
+
}) {
|
|
175
|
+
return encodeUtf8(
|
|
176
|
+
`${configSchemaVersion}\0${input.environmentName}\0${input.url}\0${input.local}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function encodeUtf8(value: string) {
|
|
181
|
+
return textEncoder.encode(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function decodeUtf8(value: Uint8Array) {
|
|
185
|
+
return textDecoder.decode(value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function decodeBase64Field(value: string, field: string) {
|
|
189
|
+
return Effect.fromResult(Encoding.decodeBase64(value)).pipe(
|
|
190
|
+
Effect.catchTags({
|
|
191
|
+
EncodingError: (error) =>
|
|
192
|
+
Effect.fail(new ConfigError({ message: `invalid encrypted token ${field}`, cause: error })),
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function mapCredentialCipherError(error: CredentialCipherError) {
|
|
198
|
+
return new ConfigError({
|
|
199
|
+
message:
|
|
200
|
+
error.operation === "encrypt"
|
|
201
|
+
? "failed to encrypt credential cipher payload"
|
|
202
|
+
: "failed to decrypt credential cipher payload",
|
|
203
|
+
cause: error,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
2
|
+
|
|
3
|
+
export function t3CliEnvConfigLayer(
|
|
4
|
+
homeDir: string,
|
|
5
|
+
env: Readonly<Record<string, string | undefined>> = {},
|
|
6
|
+
) {
|
|
7
|
+
const record: Record<string, string> = { HOME: homeDir };
|
|
8
|
+
for (const [key, value] of Object.entries(env)) {
|
|
9
|
+
if (value !== undefined) {
|
|
10
|
+
record[key] = value;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return ConfigProvider.layer(ConfigProvider.fromEnv({ env: record }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as Config from "effect/Config";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import type * as Redacted from "effect/Redacted";
|
|
5
|
+
|
|
6
|
+
export type T3CliEnvScope = {
|
|
7
|
+
readonly t3codeProjectRoot?: string | undefined;
|
|
8
|
+
readonly t3codeProjectId?: string | undefined;
|
|
9
|
+
readonly t3codeWorktreePath?: string | undefined;
|
|
10
|
+
readonly t3codeThreadId?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type T3CliEnvShape = {
|
|
14
|
+
readonly home: Option.Option<string>;
|
|
15
|
+
readonly xdgConfigHome: Option.Option<string>;
|
|
16
|
+
readonly t3cliEnv: Option.Option<string>;
|
|
17
|
+
readonly t3codeUrl: Option.Option<string>;
|
|
18
|
+
readonly t3codeToken: Option.Option<Redacted.Redacted>;
|
|
19
|
+
readonly t3codeHome: Option.Option<string>;
|
|
20
|
+
readonly term: Option.Option<string>;
|
|
21
|
+
readonly ci: boolean;
|
|
22
|
+
readonly codexCi: boolean;
|
|
23
|
+
readonly codexThreadId: boolean;
|
|
24
|
+
readonly t3cliAgent: boolean;
|
|
25
|
+
readonly scope: T3CliEnvScope;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const T3CliEnvConfig = Config.all({
|
|
29
|
+
home: Config.string("HOME").pipe(Config.option),
|
|
30
|
+
xdgConfigHome: Config.string("XDG_CONFIG_HOME").pipe(Config.option),
|
|
31
|
+
t3cliEnv: Config.string("T3CLI_ENV").pipe(Config.option),
|
|
32
|
+
t3codeUrl: Config.string("T3CODE_URL").pipe(Config.option),
|
|
33
|
+
t3codeToken: Config.redacted("T3CODE_TOKEN").pipe(Config.option),
|
|
34
|
+
t3codeHome: Config.string("T3CODE_HOME").pipe(Config.option),
|
|
35
|
+
t3codeProjectRoot: Config.string("T3CODE_PROJECT_ROOT").pipe(Config.option),
|
|
36
|
+
t3codeProjectId: Config.string("T3CODE_PROJECT_ID").pipe(Config.option),
|
|
37
|
+
t3codeWorktreePath: Config.string("T3CODE_WORKTREE_PATH").pipe(Config.option),
|
|
38
|
+
t3codeThreadId: Config.string("T3CODE_THREAD_ID").pipe(Config.option),
|
|
39
|
+
term: Config.string("TERM").pipe(Config.option),
|
|
40
|
+
ci: Config.string("CI").pipe(Config.option, Config.map(Option.isSome)),
|
|
41
|
+
codexCi: Config.string("CODEX_CI").pipe(Config.option, Config.map(Option.isSome)),
|
|
42
|
+
codexThreadId: Config.string("CODEX_THREAD_ID").pipe(Config.option, Config.map(Option.isSome)),
|
|
43
|
+
t3cliAgent: Config.string("T3CLI_AGENT").pipe(Config.option, Config.map(Option.isSome)),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function toScope(loaded: {
|
|
47
|
+
readonly t3codeProjectRoot: Option.Option<string>;
|
|
48
|
+
readonly t3codeProjectId: Option.Option<string>;
|
|
49
|
+
readonly t3codeWorktreePath: Option.Option<string>;
|
|
50
|
+
readonly t3codeThreadId: Option.Option<string>;
|
|
51
|
+
}): T3CliEnvScope {
|
|
52
|
+
return {
|
|
53
|
+
...(Option.isSome(loaded.t3codeProjectRoot)
|
|
54
|
+
? { t3codeProjectRoot: loaded.t3codeProjectRoot.value }
|
|
55
|
+
: {}),
|
|
56
|
+
...(Option.isSome(loaded.t3codeProjectId)
|
|
57
|
+
? { t3codeProjectId: loaded.t3codeProjectId.value }
|
|
58
|
+
: {}),
|
|
59
|
+
...(Option.isSome(loaded.t3codeWorktreePath)
|
|
60
|
+
? { t3codeWorktreePath: loaded.t3codeWorktreePath.value }
|
|
61
|
+
: {}),
|
|
62
|
+
...(Option.isSome(loaded.t3codeThreadId)
|
|
63
|
+
? { t3codeThreadId: loaded.t3codeThreadId.value }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const loadT3CliEnv = Effect.map(T3CliEnvConfig, (loaded) => ({
|
|
69
|
+
home: loaded.home,
|
|
70
|
+
xdgConfigHome: loaded.xdgConfigHome,
|
|
71
|
+
t3cliEnv: loaded.t3cliEnv,
|
|
72
|
+
t3codeUrl: loaded.t3codeUrl,
|
|
73
|
+
t3codeToken: loaded.t3codeToken,
|
|
74
|
+
t3codeHome: loaded.t3codeHome,
|
|
75
|
+
term: loaded.term,
|
|
76
|
+
ci: loaded.ci,
|
|
77
|
+
codexCi: loaded.codexCi,
|
|
78
|
+
codexThreadId: loaded.codexThreadId,
|
|
79
|
+
t3cliAgent: loaded.t3cliAgent,
|
|
80
|
+
scope: toScope(loaded),
|
|
81
|
+
}));
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
1
|
import * as Effect from "effect/Effect";
|
|
3
2
|
import * as Path from "effect/Path";
|
|
4
3
|
|
|
@@ -8,14 +7,6 @@ export type T3Layout = {
|
|
|
8
7
|
readonly t3codeHome?: string | undefined;
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
export function readT3LayoutFromNodeProcess(): T3Layout {
|
|
12
|
-
return {
|
|
13
|
-
cwd: process.cwd(),
|
|
14
|
-
homeDir: homedir(),
|
|
15
|
-
t3codeHome: process.env["T3CODE_HOME"],
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
export const resolveT3BaseDir = Effect.fn("resolveT3BaseDir")(function* (input: {
|
|
20
11
|
readonly layout: T3Layout;
|
|
21
12
|
readonly baseDir?: string | undefined;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Exit from "effect/Exit";
|
|
5
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
6
|
+
|
|
7
|
+
import { expectFailWithMessage } from "../../effect.test-utils.ts";
|
|
8
|
+
import { ConfigError } from "../error.ts";
|
|
9
|
+
import {
|
|
10
|
+
defaultEnvironmentNameForLocal,
|
|
11
|
+
defaultEnvironmentNameFromUrl,
|
|
12
|
+
migrateV1EnvironmentName,
|
|
13
|
+
slugifyEnvironmentName,
|
|
14
|
+
validateEnvironmentName,
|
|
15
|
+
} from "./name.ts";
|
|
16
|
+
|
|
17
|
+
describe("slugifyEnvironmentName", () => {
|
|
18
|
+
it("slugifies invalid characters", () => {
|
|
19
|
+
assert.equal(slugifyEnvironmentName("app.example.com"), "app.example.com");
|
|
20
|
+
assert.equal(slugifyEnvironmentName("---"), "default");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("validateEnvironmentName", () => {
|
|
25
|
+
it.effect("rejects empty and invalid names", () =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const empty = yield* validateEnvironmentName("").pipe(Effect.exit);
|
|
28
|
+
assert.equal(Exit.isFailure(empty), true);
|
|
29
|
+
const invalid = yield* validateEnvironmentName("bad name").pipe(Effect.exit);
|
|
30
|
+
assert.equal(Exit.isFailure(invalid), true);
|
|
31
|
+
yield* validateEnvironmentName("valid.name-1");
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("defaultEnvironmentNameFromUrl", () => {
|
|
37
|
+
it.effect("derives hostname from normalized url", () =>
|
|
38
|
+
Effect.gen(function* () {
|
|
39
|
+
const name = yield* defaultEnvironmentNameFromUrl("https://app.example.com/path");
|
|
40
|
+
assert.equal(name, "app.example.com");
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
it.effect("maps invalid urls to ConfigError", () =>
|
|
45
|
+
expectFailWithMessage(defaultEnvironmentNameFromUrl("not-a-url"), ConfigError, "invalid url"),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("migrateV1EnvironmentName", () => {
|
|
50
|
+
it.effect("uses local name for local configs", () =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const name = yield* migrateV1EnvironmentName({ local: true });
|
|
53
|
+
assert.equal(name, defaultEnvironmentNameForLocal());
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
it.effect("derives name from url when present", () =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const name = yield* migrateV1EnvironmentName({
|
|
60
|
+
url: "https://work.example",
|
|
61
|
+
local: false,
|
|
62
|
+
});
|
|
63
|
+
assert.equal(name, "work.example");
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
|
|
3
|
+
import { ConfigError } from "../error.ts";
|
|
4
|
+
import { normalizeHttpBaseUrl } from "../url/url.ts";
|
|
5
|
+
|
|
6
|
+
const environmentNamePattern = /^[A-Za-z0-9._-]+$/;
|
|
7
|
+
|
|
8
|
+
export function slugifyEnvironmentName(value: string) {
|
|
9
|
+
const slug = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
10
|
+
return slug.length > 0 ? slug : "default";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateEnvironmentName(name: string) {
|
|
14
|
+
if (name.length === 0 || !environmentNamePattern.test(name)) {
|
|
15
|
+
return Effect.fail(
|
|
16
|
+
new ConfigError({
|
|
17
|
+
message: `invalid environment name '${name}': use non-empty [A-Za-z0-9._-]`,
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return Effect.void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defaultEnvironmentNameFromUrl(url: string) {
|
|
25
|
+
return normalizeHttpBaseUrl(url).pipe(
|
|
26
|
+
Effect.mapError((error) => new ConfigError({ message: error.message, cause: error.cause })),
|
|
27
|
+
Effect.flatMap((normalized) => {
|
|
28
|
+
const hostname = slugifyEnvironmentName(new URL(normalized).hostname.trim());
|
|
29
|
+
return validateEnvironmentName(hostname).pipe(Effect.as(hostname));
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultEnvironmentNameForLocal() {
|
|
35
|
+
return "local" as const;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function migrateV1EnvironmentName(input: {
|
|
39
|
+
readonly url?: string;
|
|
40
|
+
readonly local?: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
if (input.local === true) {
|
|
43
|
+
return Effect.succeed(defaultEnvironmentNameForLocal());
|
|
44
|
+
}
|
|
45
|
+
if (input.url !== undefined && input.url.length > 0) {
|
|
46
|
+
return defaultEnvironmentNameFromUrl(input.url);
|
|
47
|
+
}
|
|
48
|
+
return Effect.succeed("default");
|
|
49
|
+
}
|
package/src/config/error.ts
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
import * as Cause from "effect/Cause";
|
|
2
1
|
import * as Schema from "effect/Schema";
|
|
3
2
|
import { PlatformError } from "effect/PlatformError";
|
|
4
3
|
|
|
5
4
|
const ConfigErrorCauseSchema = Schema.Union([
|
|
6
5
|
Schema.instanceOf(PlatformError),
|
|
7
6
|
Schema.instanceOf(Schema.SchemaError),
|
|
7
|
+
Schema.Defect(),
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
|
-
export class UrlError extends Schema.TaggedErrorClass<UrlError>()("UrlError", {
|
|
11
|
-
message: Schema.String,
|
|
12
|
-
protocol: Schema.optionalKey(Schema.String),
|
|
13
|
-
cause: Schema.optionalKey(Schema.instanceOf(Cause.IllegalArgumentError)),
|
|
14
|
-
}) {}
|
|
15
|
-
|
|
16
10
|
export class ConfigError extends Schema.TaggedErrorClass<ConfigError>()("ConfigError", {
|
|
17
11
|
message: Schema.String,
|
|
18
12
|
cause: Schema.optionalKey(ConfigErrorCauseSchema),
|
|
19
13
|
}) {}
|
|
20
14
|
|
|
21
|
-
export type ConfigServiceError = ConfigError
|
|
15
|
+
export type ConfigServiceError = ConfigError;
|
|
16
|
+
|
|
17
|
+
export function isPlatformNotFoundError(error: PlatformError) {
|
|
18
|
+
return error.reason["_tag"] === "NotFound";
|
|
19
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
export
|
|
2
|
-
export type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export * as Config from "./config.ts";
|
|
2
|
+
export type {
|
|
3
|
+
EncryptedConfig,
|
|
4
|
+
EncryptedToken,
|
|
5
|
+
EnvironmentSummary,
|
|
6
|
+
ResolvedConfig,
|
|
7
|
+
UpsertEnvironmentInput,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
export { ConfigError, type ConfigServiceError } from "./error.ts";
|
|
10
|
+
export * as Credential from "./credential/index.ts";
|
|
11
|
+
export * as Keystore from "./keystore/index.ts";
|
|
12
|
+
export * as Selection from "./selection/index.ts";
|
|
13
|
+
export * as Env from "./env/index.ts";
|
|
14
|
+
export * as Paths from "./paths/index.ts";
|
|
15
|
+
export * as Url from "./url/index.ts";
|
|
16
|
+
export * as EnvironmentName from "./environment-name/index.ts";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
|
|
3
|
+
export class KeystoreUnavailableError extends Schema.TaggedErrorClass<KeystoreUnavailableError>()(
|
|
4
|
+
"KeystoreUnavailableError",
|
|
5
|
+
{
|
|
6
|
+
reason: Schema.Literals(["module-not-found", "backend-unavailable"]),
|
|
7
|
+
cause: Schema.Defect(),
|
|
8
|
+
},
|
|
9
|
+
) {}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Encoding from "effect/Encoding";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Path from "effect/Path";
|
|
5
|
+
|
|
6
|
+
import { ConfigError, isPlatformNotFoundError } from "../error.ts";
|
|
7
|
+
import {
|
|
8
|
+
masterKeyByteLength,
|
|
9
|
+
type MasterKeyKeystore,
|
|
10
|
+
type MasterKeyReadResult,
|
|
11
|
+
} from "./service.ts";
|
|
12
|
+
import { resolveKeyFilePath } from "../paths/paths.ts";
|
|
13
|
+
|
|
14
|
+
const privateFileMode = 0o600;
|
|
15
|
+
|
|
16
|
+
export const makeFileKeystore = Effect.fn("makeFileKeystore")(function* () {
|
|
17
|
+
const fs = yield* FileSystem.FileSystem;
|
|
18
|
+
const path = yield* Path.Path;
|
|
19
|
+
const keyFilePath = yield* resolveKeyFilePath();
|
|
20
|
+
|
|
21
|
+
const read = (): Effect.Effect<MasterKeyReadResult, ConfigError> =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const raw = yield* fs.readFileString(keyFilePath).pipe(
|
|
24
|
+
Effect.catchTags({
|
|
25
|
+
PlatformError: (error) =>
|
|
26
|
+
isPlatformNotFoundError(error)
|
|
27
|
+
? Effect.succeed(undefined)
|
|
28
|
+
: Effect.fail(
|
|
29
|
+
new ConfigError({ message: "failed to read credential key file", cause: error }),
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
if (raw === undefined) {
|
|
34
|
+
return { kind: "missing" };
|
|
35
|
+
}
|
|
36
|
+
const key = yield* Effect.fromResult(Encoding.decodeBase64(raw.trim())).pipe(
|
|
37
|
+
Effect.catchTags({
|
|
38
|
+
EncodingError: (error) =>
|
|
39
|
+
Effect.fail(
|
|
40
|
+
new ConfigError({
|
|
41
|
+
message: "invalid credential key file: invalid base64",
|
|
42
|
+
cause: error,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
if (key.byteLength !== masterKeyByteLength) {
|
|
48
|
+
return yield* Effect.fail(
|
|
49
|
+
new ConfigError({ message: "invalid credential key file: unexpected key length" }),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
yield* fs.chmod(keyFilePath, privateFileMode).pipe(
|
|
53
|
+
Effect.mapError(
|
|
54
|
+
(error) =>
|
|
55
|
+
new ConfigError({
|
|
56
|
+
message: "failed to set credential key file permissions",
|
|
57
|
+
cause: error,
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
return { kind: "present", key };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const write = (key: Uint8Array): Effect.Effect<void, ConfigError> =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
yield* fs.makeDirectory(path.dirname(keyFilePath), { recursive: true, mode: 0o700 }).pipe(
|
|
67
|
+
Effect.catchTags({
|
|
68
|
+
PlatformError: (error) =>
|
|
69
|
+
Effect.fail(
|
|
70
|
+
new ConfigError({ message: "failed to write credential key file", cause: error }),
|
|
71
|
+
),
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
yield* fs
|
|
75
|
+
.writeFileString(keyFilePath, `${Encoding.encodeBase64(key)}\n`, { mode: privateFileMode })
|
|
76
|
+
.pipe(
|
|
77
|
+
Effect.catchTags({
|
|
78
|
+
PlatformError: (error) =>
|
|
79
|
+
Effect.fail(
|
|
80
|
+
new ConfigError({ message: "failed to write credential key file", cause: error }),
|
|
81
|
+
),
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
yield* fs.chmod(keyFilePath, privateFileMode).pipe(
|
|
85
|
+
Effect.mapError(
|
|
86
|
+
(error) =>
|
|
87
|
+
new ConfigError({
|
|
88
|
+
message: "failed to set credential key file permissions",
|
|
89
|
+
cause: error,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { read, write } satisfies MasterKeyKeystore;
|
|
96
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import "vite-plus/test/config";
|
|
2
|
+
|
|
3
|
+
import { assert, describe, it } from "@effect/vitest";
|
|
4
|
+
|
|
5
|
+
import { parseKeyringPassword } from "./keyring-node.ts";
|
|
6
|
+
|
|
7
|
+
describe("parseKeyringPassword", () => {
|
|
8
|
+
it("treats invalid stored keyring values as corrupt", () => {
|
|
9
|
+
const result = parseKeyringPassword("not-a-valid-key");
|
|
10
|
+
assert.equal(result.kind, "corrupt");
|
|
11
|
+
});
|
|
12
|
+
});
|