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