| @@ -1,6 +1,6 @@ |
| 1 | import { afterEach, describe, expect, it } from "@effect/vitest"; | 1 | import { afterEach, describe, expect, it } from "@effect/vitest"; |
| 2 | import { BunServices } from "@effect/platform-bun"; | 2 | import { BunServices } from "@effect/platform-bun"; |
| 3 | import { mkdtempSync, rmSync } from "node:fs"; | 3 | import { chmodSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs"; |
| 4 | import { tmpdir } from "node:os"; | 4 | import { tmpdir } from "node:os"; |
| 5 | import { join } from "node:path"; | 5 | import { join } from "node:path"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| @@ -25,7 +25,7 @@ afterEach(() => { · 18 unmodified lines |
| 25 | }); | 25 | }); |
| 26 | | 26 | |
| 27 | describe("CLI server connection profiles", () => { | 27 | describe("CLI server connection profiles", () => { |
| 28 | it("round-trips named server connections and default selection", () => | 28 | it.effect("round-trips named server connections and default selection", () => |
| 29 | Effect.gen(function* () { | 29 | Effect.gen(function* () { |
| 30 | const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-")); | 30 | const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-")); |
| 31 | process.env.EXECUTOR_DATA_DIR = dataDir; | 31 | process.env.EXECUTOR_DATA_DIR = dataDir; |
| @@ -60,9 +60,10 @@ describe("CLI server connection profiles", () => { · 28 unmodified lines |
| 60 | } finally { | 60 | } finally { |
| 61 | rmSync(dataDir, { recursive: true, force: true }); | 61 | rmSync(dataDir, { recursive: true, force: true }); |
| 62 | } | 62 | } |
| 63 | }).pipe(Effect.provide(BunServices.layer))); | 63 | }).pipe(Effect.provide(BunServices.layer)), |
| | 64 | ); |
| 64 | | 65 | |
| 65 | it("switches and removes the default profile", () => | 66 | it.effect("switches and removes the default profile", () => |
| 66 | Effect.gen(function* () { | 67 | Effect.gen(function* () { |
| 67 | const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-")); | 68 | const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-")); |
| 68 | process.env.EXECUTOR_DATA_DIR = dataDir; | 69 | process.env.EXECUTOR_DATA_DIR = dataDir; |
| @@ -88,7 +89,8 @@ describe("CLI server connection profiles", () => { · 19 unmodified lines |
| 88 | } finally { | 89 | } finally { |
| 89 | rmSync(dataDir, { recursive: true, force: true }); | 90 | rmSync(dataDir, { recursive: true, force: true }); |
| 90 | } | 91 | } |
| 91 | }).pipe(Effect.provide(BunServices.layer))); | 92 | }).pipe(Effect.provide(BunServices.layer)), |
| | 93 | ); |
| 92 | | 94 | |
| 93 | it("drops malformed profiles when parsing", () => { | 95 | it("drops malformed profiles when parsing", () => { |
| 94 | const store = parseCliServerConnectionStore( | 96 | const store = parseCliServerConnectionStore( |
| @@ -143,4 +145,68 @@ describe("CLI server connection profiles", () => { · 48 unmodified lines |
| 143 | "CF-Access-Client-Id": { kind: "env", name: "EXECUTOR_CF_ACCESS_CLIENT_ID" }, | 145 | "CF-Access-Client-Id": { kind: "env", name: "EXECUTOR_CF_ACCESS_CLIENT_ID" }, |
| 144 | }); | 146 | }); |
| 145 | }); | 147 | }); |
| | 148 | |
| | 149 | // ------------------------------------------------------------------------- |
| | 150 | // File permissions. |
| | 151 | // |
| | 152 | // This store holds live credentials for a hosted server — a bearer token, or |
| | 153 | // an OAuth access token and its long-lived refresh token. Nothing pinned its |
| | 154 | // mode before, so it was created with the process umask (0644 by default) and |
| | 155 | // readable by every other account on the machine. |
| | 156 | // |
| | 157 | // Both cases are covered because they need different mechanisms: `mode` on |
| | 158 | // create, and a `chmod` for the overwrite. The overwrite is the common path |
| | 159 | // here, since the store is rewritten on every silent token refresh. |
| | 160 | // ------------------------------------------------------------------------- |
| | 161 | |
| | 162 | it.effect("creates the credential store owner-only", () => |
| | 163 | Effect.gen(function* () { |
| | 164 | const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-mode-")); |
| | 165 | process.env.EXECUTOR_DATA_DIR = dataDir; |
| | 166 | |
| | 167 | try { |
| | 168 | yield* upsertCliServerConnectionProfile({ |
| | 169 | name: "remote", |
| | 170 | connection: { |
| | 171 | origin: "https://executor.example", |
| | 172 | auth: { kind: "bearer", token: "key_should_not_be_world_readable" }, |
| | 173 | }, |
| | 174 | makeDefault: true, |
| | 175 | }); |
| | 176 | |
| | 177 | const mode = statSync(join(dataDir, "server-connections.json")).mode & 0o777; |
| | 178 | expect(mode).toBe(0o600); |
| | 179 | } finally { |
| | 180 | rmSync(dataDir, { recursive: true, force: true }); |
| | 181 | } |
| | 182 | }).pipe(Effect.provide(BunServices.layer)), |
| | 183 | ); |
| | 184 | |
| | 185 | it.effect("tightens a pre-existing world-readable credential store on rewrite", () => |
| | 186 | Effect.gen(function* () { |
| | 187 | const dataDir = mkdtempSync(join(tmpdir(), "executor-server-profiles-chmod-")); |
| | 188 | process.env.EXECUTOR_DATA_DIR = dataDir; |
| | 189 | const storePath = join(dataDir, "server-connections.json"); |
| | 190 | |
| | 191 | try { |
| | 192 | // A store left behind by an older version, world-readable. `mode` on |
| | 193 | // write is ignored for an existing file, so only the chmod fixes this. |
| | 194 | writeFileSync(storePath, JSON.stringify({ version: 1, profiles: [] })); |
| | 195 | chmodSync(storePath, 0o644); |
| | 196 | |
| | 197 | yield* upsertCliServerConnectionProfile({ |
| | 198 | name: "remote", |
| | 199 | connection: { |
| | 200 | origin: "https://executor.example", |
| | 201 | auth: { kind: "bearer", token: "key_should_not_be_world_readable" }, |
| | 202 | }, |
| | 203 | makeDefault: true, |
| | 204 | }); |
| | 205 | |
| | 206 | expect(statSync(storePath).mode & 0o777).toBe(0o600); |
| | 207 | } finally { |
| | 208 | rmSync(dataDir, { recursive: true, force: true }); |
| | 209 | } |
| | 210 | }).pipe(Effect.provide(BunServices.layer)), |
| | 211 | ); |
| 146 | }); | 212 | }); |