authorgravatar for 206951365+cursor[bot]@users.noreply.github.comcursor[bot] <206951365+cursor[bot]@users.noreply.github.com> 2026-06-19 11:15:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 11:15:35-07:00
log13917df1fe48890f794daad8c2d94f124b18b9fe
tree62b14918fdbfadbbe5c544c77f508c70ab2894d7
parent5d4e2fae012b74cfb323cc2cb45687caf93997cc
signature Signed by PGP key B5690EEEBB952194

Use idiomatic Effect options for server secret reads (#3110)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>

13 files changed, 148 insertions(+), 124 deletions(-)

apps/server/src/auth/ServerSecretStore.test.ts+23-22
...@@ -1,10 +1,11 @@...@@ -1,10 +1,11 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import * as Cause from "effect/Cause";3import * as Cause from "effect/Cause";
4import * as Deferred from "effect/Deferred";4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
8import * as Ref from "effect/Ref";9import * as Ref from "effect/Ref";
9import * as PlatformError from "effect/PlatformError";10import * as PlatformError from "effect/PlatformError";
1011
...@@ -145,13 +146,13 @@ const makeConcurrentCreateSecretStoreLayer = () =>...@@ -145,13 +146,13 @@ const makeConcurrentCreateSecretStoreLayer = () =>
145 );146 );
146147
147it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {148it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
148 it.effect("returns null when a secret file does not exist", () =>149 it.effect("returns Option.none when a secret file does not exist", () =>
149 Effect.gen(function* () {150 Effect.gen(function* () {
150 const secretStore = yield* ServerSecretStore.ServerSecretStore;151 const secretStore = yield* ServerSecretStore.ServerSecretStore;
151152
152 const secret = yield* secretStore.get("missing-secret");153 const secret = yield* secretStore.get("missing-secret");
153154
154 expect(secret).toBeNull();155 assert.isTrue(Option.isNone(secret));
155 }).pipe(Effect.provide(makeServerSecretStoreLayer())),156 }).pipe(Effect.provide(makeServerSecretStoreLayer())),
156 );157 );
157158
...@@ -162,7 +163,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -162,7 +163,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
162 const first = yield* secretStore.getOrCreateRandom("session-signing-key", 32);163 const first = yield* secretStore.getOrCreateRandom("session-signing-key", 32);
163 const second = yield* secretStore.getOrCreateRandom("session-signing-key", 32);164 const second = yield* secretStore.getOrCreateRandom("session-signing-key", 32);
164165
165 expect(Array.from(second)).toEqual(Array.from(first));166 assert.deepEqual(Array.from(second), Array.from(first));
166 }).pipe(Effect.provide(makeServerSecretStoreLayer())),167 }).pipe(Effect.provide(makeServerSecretStoreLayer())),
167 );168 );
168169
...@@ -178,10 +179,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -178,10 +179,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
178 { concurrency: "unbounded" },179 { concurrency: "unbounded" },
179 );180 );
180 const persisted = yield* secretStore.get("session-signing-key");181 const persisted = yield* secretStore.get("session-signing-key");
182 const persistedBytes = Option.getOrThrow(persisted);
181183
182 expect(persisted).not.toBeNull();184 assert.deepEqual(Array.from(first), Array.from(persistedBytes));
183 expect(Array.from(first)).toEqual(Array.from(persisted ?? new Uint8Array()));185 assert.deepEqual(Array.from(second), Array.from(persistedBytes));
184 expect(Array.from(second)).toEqual(Array.from(persisted ?? new Uint8Array()));
185 }).pipe(Effect.provide(makeConcurrentCreateSecretStoreLayer())),186 }).pipe(Effect.provide(makeConcurrentCreateSecretStoreLayer())),
186 );187 );
187188
...@@ -217,10 +218,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -217,10 +218,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
217218
218 yield* secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3]));219 yield* secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3]));
219220
220 expect(chmodCalls.some((call) => call.mode === 0o700 && call.path.endsWith("/secrets"))).toBe(221 assert.isTrue(
221 true,222 chmodCalls.some((call) => call.mode === 0o700 && call.path.endsWith("/secrets")),
222 );223 );
223 expect(chmodCalls.filter((call) => call.mode === 0o600).length).toBeGreaterThanOrEqual(2);224 assert.isAtLeast(chmodCalls.filter((call) => call.mode === 0o600).length, 2);
224 }).pipe(Effect.provide(NodeServices.layer)),225 }).pipe(Effect.provide(NodeServices.layer)),
225 );226 );
226227
...@@ -230,10 +231,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -230,10 +231,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
230231
231 const error = yield* Effect.flip(secretStore.getOrCreateRandom("session-signing-key", 32));232 const error = yield* Effect.flip(secretStore.getOrCreateRandom("session-signing-key", 32));
232233
233 expect(error).toBeInstanceOf(ServerSecretStore.SecretStoreError);234 assert.instanceOf(error, ServerSecretStore.SecretStoreError);
234 expect(error.message).toContain("Failed to read secret session-signing-key.");235 assert.include(error.message, "Failed to read secret session-signing-key.");
235 expect(error.cause).toBeInstanceOf(PlatformError.PlatformError);236 assert.instanceOf(error.cause, PlatformError.PlatformError);
236 expect((error.cause as PlatformError.PlatformError).reason._tag).toBe("PermissionDenied");237 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");
237 }).pipe(Effect.provide(makePermissionDeniedSecretStoreLayer())),238 }).pipe(Effect.provide(makePermissionDeniedSecretStoreLayer())),
238 );239 );
239240
...@@ -245,10 +246,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -245,10 +246,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
245 secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3])),246 secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3])),
246 );247 );
247248
248 expect(error).toBeInstanceOf(ServerSecretStore.SecretStoreError);249 assert.instanceOf(error, ServerSecretStore.SecretStoreError);
249 expect(error.message).toContain("Failed to persist secret session-signing-key.");250 assert.include(error.message, "Failed to persist secret session-signing-key.");
250 expect(error.cause).toBeInstanceOf(PlatformError.PlatformError);251 assert.instanceOf(error.cause, PlatformError.PlatformError);
251 expect((error.cause as PlatformError.PlatformError).reason._tag).toBe("PermissionDenied");252 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");
252 }).pipe(Effect.provide(makeRenameFailureSecretStoreLayer())),253 }).pipe(Effect.provide(makeRenameFailureSecretStoreLayer())),
253 );254 );
254255
...@@ -258,10 +259,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {...@@ -258,10 +259,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => {
258259
259 const error = yield* Effect.flip(secretStore.remove("session-signing-key"));260 const error = yield* Effect.flip(secretStore.remove("session-signing-key"));
260261
261 expect(error).toBeInstanceOf(ServerSecretStore.SecretStoreError);262 assert.instanceOf(error, ServerSecretStore.SecretStoreError);
262 expect(error.message).toContain("Failed to remove secret session-signing-key.");263 assert.include(error.message, "Failed to remove secret session-signing-key.");
263 expect(error.cause).toBeInstanceOf(PlatformError.PlatformError);264 assert.instanceOf(error.cause, PlatformError.PlatformError);
264 expect((error.cause as PlatformError.PlatformError).reason._tag).toBe("PermissionDenied");265 assert.equal((error.cause as PlatformError.PlatformError).reason._tag, "PermissionDenied");
265 }).pipe(Effect.provide(makeRemoveFailureSecretStoreLayer())),266 }).pipe(Effect.provide(makeRemoveFailureSecretStoreLayer())),
266 );267 );
267});268});
apps/server/src/auth/ServerSecretStore.ts+47-41
...@@ -1,19 +1,23 @@...@@ -1,19 +1,23 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Crypto from "effect/Crypto";2import * as Crypto from "effect/Crypto";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Path from "effect/Path";7import * as Path from "effect/Path";
8import * as Predicate from "effect/Predicate";8import * as Predicate from "effect/Predicate";
9import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
10import * as Schema from "effect/Schema";
1011
11import { ServerConfig } from "../config.ts";12import { ServerConfig } from "../config.ts";
1213
13export class SecretStoreError extends Data.TaggedError("SecretStoreError")<{14export class SecretStoreError extends Schema.TaggedErrorClass<SecretStoreError>()(
14 readonly message: string;15 "SecretStoreError",
15 readonly cause?: unknown;16 {
16}> {}17 message: Schema.String,
18 cause: Schema.optional(Schema.Defect()),
19 },
20) {}
1721
18const isPlatformError = (value: unknown): value is PlatformError.PlatformError =>22const isPlatformError = (value: unknown): value is PlatformError.PlatformError =>
19 Predicate.isTagged(value, "PlatformError");23 Predicate.isTagged(value, "PlatformError");
...@@ -22,7 +26,7 @@ export const isSecretAlreadyExistsError = (error: SecretStoreError): boolean =>...@@ -22,7 +26,7 @@ export const isSecretAlreadyExistsError = (error: SecretStoreError): boolean =>
22 isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists";26 isPlatformError(error.cause) && error.cause.reason._tag === "AlreadyExists";
2327
24export interface ServerSecretStoreShape {28export interface ServerSecretStoreShape {
25 readonly get: (name: string) => Effect.Effect<Uint8Array | null, SecretStoreError>;29 readonly get: (name: string) => Effect.Effect<Option.Option<Uint8Array>, SecretStoreError>;
26 readonly set: (name: string, value: Uint8Array) => Effect.Effect<void, SecretStoreError>;30 readonly set: (name: string, value: Uint8Array) => Effect.Effect<void, SecretStoreError>;
27 readonly create: (name: string, value: Uint8Array) => Effect.Effect<void, SecretStoreError>;31 readonly create: (name: string, value: Uint8Array) => Effect.Effect<void, SecretStoreError>;
28 readonly getOrCreateRandom: (32 readonly getOrCreateRandom: (
...@@ -57,10 +61,10 @@ export const make = Effect.fn("makeServerSecretStore")(function* () {...@@ -57,10 +61,10 @@ export const make = Effect.fn("makeServerSecretStore")(function* () {
5761
58 const get: ServerSecretStoreShape["get"] = (name) =>62 const get: ServerSecretStoreShape["get"] = (name) =>
59 fileSystem.readFile(resolveSecretPath(name)).pipe(63 fileSystem.readFile(resolveSecretPath(name)).pipe(
60 Effect.map((bytes) => Uint8Array.from(bytes)),64 Effect.map((bytes) => Option.some(Uint8Array.from(bytes))),
61 Effect.catch((cause) =>65 Effect.catch((cause) =>
62 cause.reason._tag === "NotFound"66 cause.reason._tag === "NotFound"
63 ? Effect.succeed(null)67 ? Effect.succeed(Option.none())
64 : Effect.fail(68 : Effect.fail(
65 new SecretStoreError({69 new SecretStoreError({
66 message: `Failed to read secret ${name}.`,70 message: `Failed to read secret ${name}.`,
...@@ -133,41 +137,43 @@ export const make = Effect.fn("makeServerSecretStore")(function* () {...@@ -133,41 +137,43 @@ export const make = Effect.fn("makeServerSecretStore")(function* () {
133137
134 const getOrCreateRandom: ServerSecretStoreShape["getOrCreateRandom"] = (name, bytes) =>138 const getOrCreateRandom: ServerSecretStoreShape["getOrCreateRandom"] = (name, bytes) =>
135 get(name).pipe(139 get(name).pipe(
136 Effect.flatMap((existing) => {140 Effect.flatMap(
137 if (existing) {141 Option.match({
138 return Effect.succeed(existing);142 onSome: Effect.succeed,
139 }143 onNone: () =>
140144 crypto.randomBytes(bytes).pipe(
141 return crypto.randomBytes(bytes).pipe(145 Effect.mapError(
142 Effect.mapError(146 (cause) =>
143 (cause) =>147 new SecretStoreError({
144 new SecretStoreError({148 message: `Failed to generate random bytes for secret ${name}.`,
145 message: `Failed to generate random bytes for secret ${name}.`,149 cause,
146 cause,150 }),
147 }),151 ),
148 ),152 Effect.flatMap((generated) =>
149 Effect.flatMap((generated) =>153 create(name, generated).pipe(
150 create(name, generated).pipe(154 Effect.as(Uint8Array.from(generated)),
151 Effect.as(Uint8Array.from(generated)),155 Effect.catchTag("SecretStoreError", (error) =>
152 Effect.catchTag("SecretStoreError", (error) =>156 isSecretAlreadyExistsError(error)
153 isSecretAlreadyExistsError(error)157 ? get(name).pipe(
154 ? get(name).pipe(158 Effect.flatMap(
155 Effect.flatMap((created) =>159 Option.match({
156 created !== null160 onSome: Effect.succeed,
157 ? Effect.succeed(created)161 onNone: () =>
158 : Effect.fail(162 Effect.fail(
159 new SecretStoreError({163 new SecretStoreError({
160 message: `Failed to read secret ${name} after concurrent creation.`,164 message: `Failed to read secret ${name} after concurrent creation.`,
161 }),165 }),
162 ),166 ),
163 ),167 }),
164 )168 ),
165 : Effect.fail(error),169 )
170 : Effect.fail(error),
171 ),
172 ),
166 ),173 ),
167 ),174 ),
168 ),175 }),
169 );176 ),
170 }),
171 Effect.withSpan("ServerSecretStore.getOrCreateRandom"),177 Effect.withSpan("ServerSecretStore.getOrCreateRandom"),
172 );178 );
173179
apps/server/src/cli/connect.ts+3-3
...@@ -385,9 +385,9 @@ const connectStatusCommand = Command.make("status", {...@@ -385,9 +385,9 @@ const connectStatusCommand = Command.make("status", {
385 const status: CloudCliStatus = {385 const status: CloudCliStatus = {
386 desired,386 desired,
387 authenticated,387 authenticated,
388 linked: cloudUserId !== null,388 linked: Option.isSome(cloudUserId),
389 cloudUserId: cloudUserId ? bytesToString(cloudUserId) : null,389 cloudUserId: Option.isSome(cloudUserId) ? bytesToString(cloudUserId.value) : null,
390 relayUrl: relayUrl ? bytesToString(relayUrl) : null,390 relayUrl: Option.isSome(relayUrl) ? bytesToString(relayUrl.value) : null,
391 relayClient: executable,391 relayClient: executable,
392 };392 };
393 yield* Console.log(formatCloudStatus(status, { json: flags.json }));393 yield* Console.log(formatCloudStatus(status, { json: flags.json }));
apps/server/src/cloud/CliState.test.ts+6-5
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
56
6import * as ServerSecretStore from "../auth/ServerSecretStore.ts";7import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
7import { ServerConfig } from "../config.ts";8import { ServerConfig } from "../config.ts";
...@@ -40,18 +41,18 @@ it.layer(NodeServices.layer)("CliState", (it) => {...@@ -40,18 +41,18 @@ it.layer(NodeServices.layer)("CliState", (it) => {
40 Effect.gen(function* () {41 Effect.gen(function* () {
41 const secrets = yield* ServerSecretStore.ServerSecretStore;42 const secrets = yield* ServerSecretStore.ServerSecretStore;
4243
43 expect(yield* CliState.readCliDesiredCloudLink).toBe(false);44 assert.isFalse(yield* CliState.readCliDesiredCloudLink);
44 yield* CliState.setCliDesiredCloudLink(true);45 yield* CliState.setCliDesiredCloudLink(true);
45 expect(yield* CliState.readCliDesiredCloudLink).toBe(true);46 assert.isTrue(yield* CliState.readCliDesiredCloudLink);
4647
47 for (const name of persistedCloudLinkSecrets) {48 for (const name of persistedCloudLinkSecrets) {
48 yield* secrets.set(name, new TextEncoder().encode(name));49 yield* secrets.set(name, new TextEncoder().encode(name));
49 }50 }
50 yield* CliState.clearPersistedCloudLink;51 yield* CliState.clearPersistedCloudLink;
5152
52 expect(yield* CliState.readCliDesiredCloudLink).toBe(false);53 assert.isFalse(yield* CliState.readCliDesiredCloudLink);
53 for (const name of persistedCloudLinkSecrets) {54 for (const name of persistedCloudLinkSecrets) {
54 expect(yield* secrets.get(name)).toBe(null);55 assert.isTrue(Option.isNone(yield* secrets.get(name)));
55 }56 }
56 }).pipe(Effect.provide(makeTestLayer())),57 }).pipe(Effect.provide(makeTestLayer())),
57 );58 );
apps/server/src/cloud/CliState.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import * as Effect from "effect/Effect";1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
23
3import * as ServerSecretStore from "../auth/ServerSecretStore.ts";4import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
4import {5import {
...@@ -17,7 +18,7 @@ const TRUE_BYTES = new TextEncoder().encode("true");...@@ -17,7 +18,7 @@ const TRUE_BYTES = new TextEncoder().encode("true");
1718
18export const readCliDesiredCloudLink = Effect.gen(function* () {19export const readCliDesiredCloudLink = Effect.gen(function* () {
19 const secrets = yield* ServerSecretStore.ServerSecretStore;20 const secrets = yield* ServerSecretStore.ServerSecretStore;
20 return (yield* secrets.get(CLOUD_CLI_DESIRED_LINK_SECRET)) !== null;21 return Option.isSome(yield* secrets.get(CLOUD_CLI_DESIRED_LINK_SECRET));
21});22});
2223
23export const setCliDesiredCloudLink = Effect.fn("cloud.cli_state.set_desired")(function* (24export const setCliDesiredCloudLink = Effect.fn("cloud.cli_state.set_desired")(function* (
apps/server/src/cloud/CliTokenManager.ts+2-2
...@@ -100,8 +100,8 @@ const make = Effect.gen(function* () {...@@ -100,8 +100,8 @@ const make = Effect.gen(function* () {
100100
101 const read = Effect.fn("cloud.cli_token.read")(function* () {101 const read = Effect.fn("cloud.cli_token.read")(function* () {
102 const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET);102 const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET);
103 if (!encoded) return Option.none<PersistedToken>();103 if (Option.isNone(encoded)) return Option.none<PersistedToken>();
104 return Option.some(yield* decodePersistedToken(bytesToString(encoded)));104 return Option.some(yield* decodePersistedToken(bytesToString(encoded.value)));
105 });105 });
106106
107 const exchangeToken = Effect.fn("cloud.cli_token.exchange")(function* (107 const exchangeToken = Effect.fn("cloud.cli_token.exchange")(function* (
apps/server/src/cloud/ManagedEndpointRuntime.ts+2-2
...@@ -22,10 +22,10 @@ function bytesToString(bytes: Uint8Array): string {...@@ -22,10 +22,10 @@ function bytesToString(bytes: Uint8Array): string {
22const readRuntimeConfig = Effect.gen(function* () {22const readRuntimeConfig = Effect.gen(function* () {
23 const secrets = yield* ServerSecretStore.ServerSecretStore;23 const secrets = yield* ServerSecretStore.ServerSecretStore;
24 const bytes = yield* secrets.get(CLOUD_ENDPOINT_RUNTIME_CONFIG);24 const bytes = yield* secrets.get(CLOUD_ENDPOINT_RUNTIME_CONFIG);
25 if (!bytes) {25 if (Option.isNone(bytes)) {
26 return null;26 return null;
27 }27 }
28 return Option.getOrNull(decodeRuntimeConfig(bytesToString(bytes)));28 return Option.getOrNull(decodeRuntimeConfig(bytesToString(bytes.value)));
29});29});
3030
31export interface CloudManagedEndpointRuntimeShape {31export interface CloudManagedEndpointRuntimeShape {
apps/server/src/cloud/environmentKeys.test.ts+12-9
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
5import * as PlatformError from "effect/PlatformError";6import * as PlatformError from "effect/PlatformError";
67
7import * as ServerSecretStore from "../auth/ServerSecretStore.ts";8import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
...@@ -23,10 +24,10 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it...@@ -23,10 +24,10 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it
23 const first = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore);24 const first = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore);
24 const second = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore);25 const second = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore);
2526
26 expect(second).toEqual(first);27 assert.deepEqual(second, first);
27 expect(yield* secretStore.get("cloud-link-ed25519-key-pair")).not.toBeNull();28 assert.isTrue(Option.isSome(yield* secretStore.get("cloud-link-ed25519-key-pair")));
28 expect(yield* secretStore.get("cloud-link-ed25519-private-key")).toBeNull();29 assert.isTrue(Option.isNone(yield* secretStore.get("cloud-link-ed25519-private-key")));
29 expect(yield* secretStore.get("cloud-link-ed25519-public-key")).toBeNull();30 assert.isTrue(Option.isNone(yield* secretStore.get("cloud-link-ed25519-public-key")));
30 }).pipe(Effect.provide(makeServerSecretStoreLayer())),31 }).pipe(Effect.provide(makeServerSecretStoreLayer())),
31 );32 );
3233
...@@ -36,11 +37,11 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it...@@ -36,11 +37,11 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it
36 yield* secretStore.set("cloud-link-ed25519-private-key", new TextEncoder().encode("private"));37 yield* secretStore.set("cloud-link-ed25519-private-key", new TextEncoder().encode("private"));
37 yield* secretStore.set("cloud-link-ed25519-public-key", new TextEncoder().encode("public"));38 yield* secretStore.set("cloud-link-ed25519-public-key", new TextEncoder().encode("public"));
3839
39 expect(yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore)).toEqual({40 assert.deepEqual(yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore), {
40 privateKey: "private",41 privateKey: "private",
41 publicKey: "public",42 publicKey: "public",
42 });43 });
43 expect(yield* secretStore.get("cloud-link-ed25519-key-pair")).not.toBeNull();44 assert.isTrue(Option.isSome(yield* secretStore.get("cloud-link-ed25519-key-pair")));
44 }).pipe(Effect.provide(makeServerSecretStoreLayer())),45 }).pipe(Effect.provide(makeServerSecretStoreLayer())),
45 );46 );
4647
...@@ -53,7 +54,9 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it...@@ -53,7 +54,9 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it
53 const secretStore = {54 const secretStore = {
54 get: (name) =>55 get: (name) =>
55 Effect.sync(() =>56 Effect.sync(() =>
56 name === "cloud-link-ed25519-key-pair" && createAttempted ? winner : null,57 name === "cloud-link-ed25519-key-pair" && createAttempted
58 ? Option.some(winner)
59 : Option.none(),
57 ),60 ),
58 set: unusedSecretStoreOperation,61 set: unusedSecretStoreOperation,
59 create: () =>62 create: () =>
...@@ -78,7 +81,7 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it...@@ -78,7 +81,7 @@ it.layer(NodeServices.layer)("getOrCreateEnvironmentKeyPairFromSecretStore", (it
78 remove: unusedSecretStoreOperation,81 remove: unusedSecretStoreOperation,
79 } satisfies ServerSecretStore.ServerSecretStoreShape;82 } satisfies ServerSecretStore.ServerSecretStoreShape;
8083
81 expect(yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore)).toEqual({84 assert.deepEqual(yield* getOrCreateEnvironmentKeyPairFromSecretStore(secretStore), {
82 privateKey: "winner-private",85 privateKey: "winner-private",
83 publicKey: "winner-public",86 publicKey: "winner-public",
84 });87 });
apps/server/src/cloud/environmentKeys.ts+16-12
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import * as NodeCrypto from "node:crypto";1import * as NodeCrypto from "node:crypto";
2import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
45
5import * as ServerSecretStore from "../auth/ServerSecretStore.ts";6import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
...@@ -33,14 +34,15 @@ const readEnvironmentKeyPair = Effect.fn("readEnvironmentKeyPair")(function* (...@@ -33,14 +34,15 @@ const readEnvironmentKeyPair = Effect.fn("readEnvironmentKeyPair")(function* (
33 secrets: ServerSecretStore.ServerSecretStoreShape,34 secrets: ServerSecretStore.ServerSecretStoreShape,
34) {35) {
35 const encoded = yield* secrets.get(CLOUD_LINK_KEY_PAIR);36 const encoded = yield* secrets.get(CLOUD_LINK_KEY_PAIR);
36 if (encoded === null) {37 if (Option.isNone(encoded)) {
37 return null;38 return Option.none<EnvironmentKeyPair>();
38 }39 }
39 return yield* decodeEnvironmentKeyPair(bytesToString(encoded)).pipe(40 const decoded = yield* decodeEnvironmentKeyPair(bytesToString(encoded.value)).pipe(
40 Effect.mapError((cause) =>41 Effect.mapError((cause) =>
41 keyPairPersistenceError("Failed to decode environment signing key pair.", cause),42 keyPairPersistenceError("Failed to decode environment signing key pair.", cause),
42 ),43 ),
43 );44 );
45 return Option.some(decoded);
44});46});
4547
46const persistEnvironmentKeyPair = Effect.fn("persistEnvironmentKeyPair")(function* (48const persistEnvironmentKeyPair = Effect.fn("persistEnvironmentKeyPair")(function* (
...@@ -57,14 +59,16 @@ const persistEnvironmentKeyPair = Effect.fn("persistEnvironmentKeyPair")(functio...@@ -57,14 +59,16 @@ const persistEnvironmentKeyPair = Effect.fn("persistEnvironmentKeyPair")(functio
57 Effect.catchTag("SecretStoreError", (error) =>59 Effect.catchTag("SecretStoreError", (error) =>
58 ServerSecretStore.isSecretAlreadyExistsError(error)60 ServerSecretStore.isSecretAlreadyExistsError(error)
59 ? readEnvironmentKeyPair(secrets).pipe(61 ? readEnvironmentKeyPair(secrets).pipe(
60 Effect.flatMap((existing) =>62 Effect.flatMap(
61 existing !== null63 Option.match({
62 ? Effect.succeed(existing)64 onSome: Effect.succeed,
63 : Effect.fail(65 onNone: () =>
66 Effect.fail(
64 keyPairPersistenceError(67 keyPairPersistenceError(
65 "Failed to read environment signing key pair after concurrent creation.",68 "Failed to read environment signing key pair after concurrent creation.",
66 ),69 ),
67 ),70 ),
71 }),
68 ),72 ),
69 )73 )
70 : Effect.fail(error),74 : Effect.fail(error),
...@@ -76,16 +80,16 @@ export const getOrCreateEnvironmentKeyPairFromSecretStore = Effect.fn(function*...@@ -76,16 +80,16 @@ export const getOrCreateEnvironmentKeyPairFromSecretStore = Effect.fn(function*
76 secrets: ServerSecretStore.ServerSecretStoreShape,80 secrets: ServerSecretStore.ServerSecretStoreShape,
77) {81) {
78 const existing = yield* readEnvironmentKeyPair(secrets);82 const existing = yield* readEnvironmentKeyPair(secrets);
79 if (existing !== null) {83 if (Option.isSome(existing)) {
80 return existing;84 return existing.value;
81 }85 }
8286
83 const existingPrivate = yield* secrets.get(CLOUD_LINK_PRIVATE_KEY);87 const existingPrivate = yield* secrets.get(CLOUD_LINK_PRIVATE_KEY);
84 const existingPublic = yield* secrets.get(CLOUD_LINK_PUBLIC_KEY);88 const existingPublic = yield* secrets.get(CLOUD_LINK_PUBLIC_KEY);
85 if (existingPrivate && existingPublic) {89 if (Option.isSome(existingPrivate) && Option.isSome(existingPublic)) {
86 return yield* persistEnvironmentKeyPair(secrets, {90 return yield* persistEnvironmentKeyPair(secrets, {
87 privateKey: bytesToString(existingPrivate),91 privateKey: bytesToString(existingPrivate.value),
88 publicKey: bytesToString(existingPublic),92 publicKey: bytesToString(existingPublic.value),
89 });93 });
90 }94 }
9195
apps/server/src/cloud/http.ts+22-22
...@@ -220,10 +220,10 @@ function validateLinkedCloudUser(input: {...@@ -220,10 +220,10 @@ function validateLinkedCloudUser(input: {
220 }),220 }),
221 ),221 ),
222 Effect.flatMap((existing) => {222 Effect.flatMap((existing) => {
223 if (!existing) {223 if (Option.isNone(existing)) {
224 return Effect.void;224 return Effect.void;
225 }225 }
226 const existingCloudUserId = bytesToString(existing);226 const existingCloudUserId = bytesToString(existing.value);
227 return existingCloudUserId === input.cloudUserId227 return existingCloudUserId === input.cloudUserId
228 ? Effect.void228 ? Effect.void
229 : Effect.fail(229 : Effect.fail(
...@@ -248,8 +248,8 @@ function readInstalledCloudUserId(...@@ -248,8 +248,8 @@ function readInstalledCloudUserId(
248 }),248 }),
249 ),249 ),
250 Effect.flatMap((bytes) =>250 Effect.flatMap((bytes) =>
251 bytes251 Option.isSome(bytes)
252 ? Effect.succeed(bytesToString(bytes))252 ? Effect.succeed(bytesToString(bytes.value))
253 : Effect.fail(253 : Effect.fail(
254 new EnvironmentAuth.ServerAuthInternalError({254 new EnvironmentAuth.ServerAuthInternalError({
255 message: "Cloud linked user is not installed for this environment.",255 message: "Cloud linked user is not installed for this environment.",
...@@ -622,12 +622,12 @@ const readCloudLinkState = Effect.fn("environment.cloud.readLinkState")(function...@@ -622,12 +622,12 @@ const readCloudLinkState = Effect.fn("environment.cloud.readLinkState")(function
622 { concurrency: 4 },622 { concurrency: 4 },
623 );623 );
624 return {624 return {
625 linked: cloudUserId !== null,625 linked: Option.isSome(cloudUserId),
626 cloudUserId: cloudUserId ? bytesToString(cloudUserId) : null,626 cloudUserId: Option.isSome(cloudUserId) ? bytesToString(cloudUserId.value) : null,
627 relayUrl: relayUrl ? bytesToString(relayUrl) : null,627 relayUrl: Option.isSome(relayUrl) ? bytesToString(relayUrl.value) : null,
628 relayIssuer: relayIssuer ? bytesToString(relayIssuer) : null,628 relayIssuer: Option.isSome(relayIssuer) ? bytesToString(relayIssuer.value) : null,
629 publishAgentActivity: publishAgentActivity629 publishAgentActivity: Option.isSome(publishAgentActivity)
630 ? bytesToString(publishAgentActivity) === "true"630 ? bytesToString(publishAgentActivity.value) === "true"
631 : false,631 : false,
632 } satisfies EnvironmentCloudLinkStateResult;632 } satisfies EnvironmentCloudLinkStateResult;
633});633});
...@@ -690,8 +690,8 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")(...@@ -690,8 +690,8 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")(
690 function* (dependencies: CloudHttpDependencies, request: RelayCloudEnvironmentHealthRequest) {690 function* (dependencies: CloudHttpDependencies, request: RelayCloudEnvironmentHealthRequest) {
691 const cloudMintPublicKey = yield* dependencies.secrets.get(CLOUD_MINT_PUBLIC_KEY).pipe(691 const cloudMintPublicKey = yield* dependencies.secrets.get(CLOUD_MINT_PUBLIC_KEY).pipe(
692 Effect.flatMap((bytes) =>692 Effect.flatMap((bytes) =>
693 bytes693 Option.isSome(bytes)
694 ? Effect.succeed(bytesToString(bytes))694 ? Effect.succeed(bytesToString(bytes.value))
695 : Effect.fail(695 : Effect.fail(
696 new EnvironmentAuth.ServerAuthInternalError({696 new EnvironmentAuth.ServerAuthInternalError({
697 message: "Cloud mint public key is not installed for this environment.",697 message: "Cloud mint public key is not installed for this environment.",
...@@ -701,12 +701,12 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")(...@@ -701,12 +701,12 @@ const cloudEnvironmentHealthHandler = Effect.fn("environment.cloud.health")(
701 );701 );
702 const relayIssuer = yield* dependencies.secrets.get(RELAY_ISSUER_SECRET).pipe(702 const relayIssuer = yield* dependencies.secrets.get(RELAY_ISSUER_SECRET).pipe(
703 Effect.flatMap((bytes) =>703 Effect.flatMap((bytes) =>
704 bytes704 Option.isSome(bytes)
705 ? Effect.succeed(bytesToString(bytes))705 ? Effect.succeed(bytesToString(bytes.value))
706 : dependencies.secrets.get(RELAY_URL_SECRET).pipe(706 : dependencies.secrets.get(RELAY_URL_SECRET).pipe(
707 Effect.flatMap((fallbackBytes) =>707 Effect.flatMap((fallbackBytes) =>
708 fallbackBytes708 Option.isSome(fallbackBytes)
709 ? Effect.succeed(bytesToString(fallbackBytes))709 ? Effect.succeed(bytesToString(fallbackBytes.value))
710 : Effect.fail(710 : Effect.fail(
711 new EnvironmentAuth.ServerAuthInternalError({711 new EnvironmentAuth.ServerAuthInternalError({
712 message: "Cloud relay issuer is not installed for this environment.",712 message: "Cloud relay issuer is not installed for this environment.",
...@@ -807,8 +807,8 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")...@@ -807,8 +807,8 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")
807 function* (dependencies: CloudHttpDependencies, request: RelayCloudMintCredentialRequest) {807 function* (dependencies: CloudHttpDependencies, request: RelayCloudMintCredentialRequest) {
808 const cloudMintPublicKey = yield* dependencies.secrets.get(CLOUD_MINT_PUBLIC_KEY).pipe(808 const cloudMintPublicKey = yield* dependencies.secrets.get(CLOUD_MINT_PUBLIC_KEY).pipe(
809 Effect.flatMap((bytes) =>809 Effect.flatMap((bytes) =>
810 bytes810 Option.isSome(bytes)
811 ? Effect.succeed(bytesToString(bytes))811 ? Effect.succeed(bytesToString(bytes.value))
812 : Effect.fail(812 : Effect.fail(
813 new EnvironmentAuth.ServerAuthInternalError({813 new EnvironmentAuth.ServerAuthInternalError({
814 message: "Cloud mint public key is not installed for this environment.",814 message: "Cloud mint public key is not installed for this environment.",
...@@ -818,12 +818,12 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")...@@ -818,12 +818,12 @@ const cloudMintCredentialHandler = Effect.fn("environment.cloud.mintCredential")
818 );818 );
819 const relayIssuer = yield* dependencies.secrets.get(RELAY_ISSUER_SECRET).pipe(819 const relayIssuer = yield* dependencies.secrets.get(RELAY_ISSUER_SECRET).pipe(
820 Effect.flatMap((bytes) =>820 Effect.flatMap((bytes) =>
821 bytes821 Option.isSome(bytes)
822 ? Effect.succeed(bytesToString(bytes))822 ? Effect.succeed(bytesToString(bytes.value))
823 : dependencies.secrets.get(RELAY_URL_SECRET).pipe(823 : dependencies.secrets.get(RELAY_URL_SECRET).pipe(
824 Effect.flatMap((fallbackBytes) =>824 Effect.flatMap((fallbackBytes) =>
825 fallbackBytes825 Option.isSome(fallbackBytes)
826 ? Effect.succeed(bytesToString(fallbackBytes))826 ? Effect.succeed(bytesToString(fallbackBytes.value))
827 : Effect.fail(827 : Effect.fail(
828 new EnvironmentAuth.ServerAuthInternalError({828 new EnvironmentAuth.ServerAuthInternalError({
829 message: "Cloud relay issuer is not installed for this environment.",829 message: "Cloud relay issuer is not installed for this environment.",
apps/server/src/relay/AgentAwarenessRelay.test.ts+4-3
...@@ -64,9 +64,10 @@ function makeMemorySecretStore() {...@@ -64,9 +64,10 @@ function makeMemorySecretStore() {
64 const values = new Map<string, Uint8Array>();64 const values = new Map<string, Uint8Array>();
65 const store = {65 const store = {
66 get: ((name) =>66 get: ((name) =>
67 Effect.sync(67 Effect.sync(() => {
68 () => values.get(name) ?? null,68 const value = values.get(name);
69 )) satisfies ServerSecretStore.ServerSecretStoreShape["get"],69 return value === undefined ? Option.none() : Option.some(Uint8Array.from(value));
70 })) satisfies ServerSecretStore.ServerSecretStoreShape["get"],
70 set: ((name, value) =>71 set: ((name, value) =>
71 Effect.sync(() => {72 Effect.sync(() => {
72 values.set(name, Uint8Array.from(value));73 values.set(name, Uint8Array.from(value));
apps/server/src/relay/AgentAwarenessRelay.ts+7-1
...@@ -276,7 +276,13 @@ const make = Effect.gen(function* () {...@@ -276,7 +276,13 @@ const make = Effect.gen(function* () {
276 const publishedStateByThreadRef = yield* Ref.make(new Map<ThreadId, string>());276 const publishedStateByThreadRef = yield* Ref.make(new Map<ThreadId, string>());
277277
278 const readSecretString = (name: string) =>278 const readSecretString = (name: string) =>
279 secrets.get(name).pipe(Effect.map((bytes) => (bytes ? new TextDecoder().decode(bytes) : null)));279 secrets
280 .get(name)
281 .pipe(
282 Effect.map((bytes) =>
283 Option.isSome(bytes) ? new TextDecoder().decode(bytes.value) : null,
284 ),
285 );
280286
281 const readRelayConfig = Effect.gen(function* () {287 const readRelayConfig = Effect.gen(function* () {
282 const [url, issuer, environmentCredential] = yield* Effect.all([288 const [url, issuer, environmentCredential] = yield* Effect.all([
apps/server/src/serverSettings.ts+2-1
...@@ -32,6 +32,7 @@ import * as Effect from "effect/Effect";...@@ -32,6 +32,7 @@ import * as Effect from "effect/Effect";
32import * as Exit from "effect/Exit";32import * as Exit from "effect/Exit";
33import * as FileSystem from "effect/FileSystem";33import * as FileSystem from "effect/FileSystem";
34import * as Layer from "effect/Layer";34import * as Layer from "effect/Layer";
35import * as Option from "effect/Option";
35import * as Path from "effect/Path";36import * as Path from "effect/Path";
36import * as Equal from "effect/Equal";37import * as Equal from "effect/Equal";
37import * as PubSub from "effect/PubSub";38import * as PubSub from "effect/PubSub";
...@@ -350,7 +351,7 @@ const makeServerSettings = Effect.gen(function* () {...@@ -350,7 +351,7 @@ const makeServerSettings = Effect.gen(function* () {
350 );351 );
351 environment.push({352 environment.push({
352 ...variable,353 ...variable,
353 value: secret ? textDecoder.decode(secret) : "",354 value: Option.isSome(secret) ? textDecoder.decode(secret.value) : "",
354 });355 });
355 }356 }
356 providerInstances[instanceId] = {357 providerInstances[instanceId] = {