| author | |
| committer | |
| log | 13917df1fe48890f794daad8c2d94f124b18b9fe |
| tree | 62b14918fdbfadbbe5c544c77f508c70ab2894d7 |
| parent | 5d4e2fae012b74cfb323cc2cb45687caf93997cc |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { expect, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Cause from "effect/Cause"; | 3 | import * as Cause from "effect/Cause"; |
| 4 | import * as Deferred from "effect/Deferred"; | 4 | import * as Deferred from "effect/Deferred"; |
| 5 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 8 | import * as Option from "effect/Option"; | ||
| 8 | import * as Ref from "effect/Ref"; | 9 | import * as Ref from "effect/Ref"; |
| 9 | import * as PlatformError from "effect/PlatformError"; | 10 | import * as PlatformError from "effect/PlatformError"; |
| 10 | 11 | ||
| ... | @@ -145,13 +146,13 @@ const makeConcurrentCreateSecretStoreLayer = () => | ... | @@ -145,13 +146,13 @@ const makeConcurrentCreateSecretStoreLayer = () => |
| 145 | ); | 146 | ); |
| 146 | 147 | ||
| 147 | it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { | 148 | it.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; |
| 151 | 152 | ||
| 152 | const secret = yield* secretStore.get("missing-secret"); | 153 | const secret = yield* secretStore.get("missing-secret"); |
| 153 | 154 | ||
| 154 | expect(secret).toBeNull(); | 155 | assert.isTrue(Option.isNone(secret)); |
| 155 | }).pipe(Effect.provide(makeServerSecretStoreLayer())), | 156 | }).pipe(Effect.provide(makeServerSecretStoreLayer())), |
| 156 | ); | 157 | ); |
| 157 | 158 | ||
| ... | @@ -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); |
| 164 | 165 | ||
| 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 | ); |
| 168 | 169 | ||
| ... | @@ -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); | ||
| 181 | 183 | ||
| 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 | ); |
| 187 | 188 | ||
| ... | @@ -217,10 +218,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { | ... | @@ -217,10 +218,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { |
| 217 | 218 | ||
| 218 | yield* secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3])); | 219 | yield* secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3])); |
| 219 | 220 | ||
| 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 | ); |
| 226 | 227 | ||
| ... | @@ -230,10 +231,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { | ... | @@ -230,10 +231,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { |
| 230 | 231 | ||
| 231 | const error = yield* Effect.flip(secretStore.getOrCreateRandom("session-signing-key", 32)); | 232 | const error = yield* Effect.flip(secretStore.getOrCreateRandom("session-signing-key", 32)); |
| 232 | 233 | ||
| 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 | ); |
| 239 | 240 | ||
| ... | @@ -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 | ); |
| 247 | 248 | ||
| 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 | ); |
| 254 | 255 | ||
| ... | @@ -258,10 +259,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { | ... | @@ -258,10 +259,10 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { |
| 258 | 259 | ||
| 259 | const error = yield* Effect.flip(secretStore.remove("session-signing-key")); | 260 | const error = yield* Effect.flip(secretStore.remove("session-signing-key")); |
| 260 | 261 | ||
| 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 @@ |
| 1 | import * as Context from "effect/Context"; | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Crypto from "effect/Crypto"; | 2 | import * as Crypto from "effect/Crypto"; |
| 3 | import * as Data from "effect/Data"; | ||
| 4 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 6 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | ||
| 7 | import * as Path from "effect/Path"; | 7 | import * as Path from "effect/Path"; |
| 8 | import * as Predicate from "effect/Predicate"; | 8 | import * as Predicate from "effect/Predicate"; |
| 9 | import * as PlatformError from "effect/PlatformError"; | 9 | import * as PlatformError from "effect/PlatformError"; |
| 10 | import * as Schema from "effect/Schema"; | ||
| 10 | 11 | ||
| 11 | import { ServerConfig } from "../config.ts"; | 12 | import { ServerConfig } from "../config.ts"; |
| 12 | 13 | ||
| 13 | export class SecretStoreError extends Data.TaggedError("SecretStoreError")<{ | 14 | export 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 | ) {} | ||
| 17 | 21 | ||
| 18 | const isPlatformError = (value: unknown): value is PlatformError.PlatformError => | 22 | const 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"; |
| 23 | 27 | ||
| 24 | export interface ServerSecretStoreShape { | 28 | export 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* () { |
| 57 | 61 | ||
| 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* () { |
| 133 | 137 | ||
| 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: () => |
| 140 | 144 | 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 !== null | 160 | 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 | ); |
| 173 | 179 |
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 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { expect, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; | ||
| 5 | 6 | ||
| 6 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 7 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 7 | import { ServerConfig } from "../config.ts"; | 8 | import { 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; |
| 42 | 43 | ||
| 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); |
| 46 | 47 | ||
| 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; |
| 51 | 52 | ||
| 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 @@ |
| 1 | import * as Effect from "effect/Effect"; | 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Option from "effect/Option"; | ||
| 2 | 3 | ||
| 3 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 4 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 4 | import { | 5 | import { |
| ... | @@ -17,7 +18,7 @@ const TRUE_BYTES = new TextEncoder().encode("true"); | ... | @@ -17,7 +18,7 @@ const TRUE_BYTES = new TextEncoder().encode("true"); |
| 17 | 18 | ||
| 18 | export const readCliDesiredCloudLink = Effect.gen(function* () { | 19 | export 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 | }); |
| 22 | 23 | ||
| 23 | export const setCliDesiredCloudLink = Effect.fn("cloud.cli_state.set_desired")(function* ( | 24 | export 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* () { |
| 100 | 100 | ||
| 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 | }); |
| 106 | 106 | ||
| 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 { |
| 22 | const readRuntimeConfig = Effect.gen(function* () { | 22 | const 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 | }); |
| 30 | 30 | ||
| 31 | export interface CloudManagedEndpointRuntimeShape { | 31 | export interface CloudManagedEndpointRuntimeShape { |
apps/server/src/cloud/environmentKeys.test.ts+12-9| ... | @@ -1,7 +1,8 @@ | ... | @@ -1,7 +1,8 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { expect, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; | 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; | ||
| 5 | import * as PlatformError from "effect/PlatformError"; | 6 | import * as PlatformError from "effect/PlatformError"; |
| 6 | 7 | ||
| 7 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 8 | import * 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); |
| 25 | 26 | ||
| 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 | ); |
| 32 | 33 | ||
| ... | @@ -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")); |
| 38 | 39 | ||
| 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 | ); |
| 46 | 47 | ||
| ... | @@ -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; |
| 80 | 83 | ||
| 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 @@ |
| 1 | import * as NodeCrypto from "node:crypto"; | 1 | import * as NodeCrypto from "node:crypto"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | ||
| 3 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| 4 | 5 | ||
| 5 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 6 | import * 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 | }); |
| 45 | 47 | ||
| 46 | const persistEnvironmentKeyPair = Effect.fn("persistEnvironmentKeyPair")(function* ( | 48 | const 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 !== null | 63 | 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 | } |
| 82 | 86 | ||
| 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 | } |
| 91 | 95 |
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.cloudUserId | 227 | return existingCloudUserId === input.cloudUserId |
| 228 | ? Effect.void | 228 | ? 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 | bytes | 251 | 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: publishAgentActivity | 629 | 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 | bytes | 693 | 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 | bytes | 704 | 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 | fallbackBytes | 708 | 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 | bytes | 810 | 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 | bytes | 821 | 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 | fallbackBytes | 825 | 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>()); |
| 277 | 277 | ||
| 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 | ); | ||
| 280 | 286 | ||
| 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"; |
| 32 | import * as Exit from "effect/Exit"; | 32 | import * as Exit from "effect/Exit"; |
| 33 | import * as FileSystem from "effect/FileSystem"; | 33 | import * as FileSystem from "effect/FileSystem"; |
| 34 | import * as Layer from "effect/Layer"; | 34 | import * as Layer from "effect/Layer"; |
| 35 | import * as Option from "effect/Option"; | ||
| 35 | import * as Path from "effect/Path"; | 36 | import * as Path from "effect/Path"; |
| 36 | import * as Equal from "effect/Equal"; | 37 | import * as Equal from "effect/Equal"; |
| 37 | import * as PubSub from "effect/PubSub"; | 38 | import * 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] = { |