119 lines · 4.5 KB · TypeScript
RawDownload
import { createClient } from "@libsql/client";import { mkdtempSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { Effect, Layer } from "effect";import { afterAll, beforeAll, expect, test } from "@effect/vitest"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk";import { makeScopedExecutor } from "@executor-js/api/server"; import { createSelfHostDb, SelfHostDb } from "./db/self-host-db";import { SelfHostScopedExecutorSeams } from "./execution";import type { SelfHostPlugins } from "./plugins"; // In v2 a connection IS the credential: its inline `value` is written through the// default writable provider — here the encrypted-secrets provider, which stores// an AES-GCM payload at rest. This test registers an integration, attaches an// org connection carrying a plaintext needle, and asserts the needle never// reaches the SQLite file while the versioned "v1." ciphertext does.const dataDir = mkdtempSync(join(tmpdir(), "eh-secrets-"));const dbPath = join(dataDir, "data.db");process.env.EXECUTOR_DATA_DIR = dataDir;process.env.EXECUTOR_SECRET_KEY = "integration-test-master-key"; const createScopedExecutor = (  accountId: string,  organizationId: string,  organizationName: string,) =>  makeScopedExecutor<SelfHostPlugins>(accountId, organizationId, organizationName).pipe(    Effect.provide(SelfHostScopedExecutorSeams),  ); let dbLayer!: Layer.Layer<SelfHostDb>;let dbHandle: Awaited<ReturnType<typeof createSelfHostDb>> | undefined; beforeAll(async () => {  dbHandle = await createSelfHostDb({    path: dbPath,    namespace: "executor_selfhost",    version: "1.0.0",  });  dbLayer = Layer.succeed(SelfHostDb)(dbHandle);});afterAll(async () => {  await dbHandle?.close();}); const TINY_SPEC = JSON.stringify({  openapi: "3.0.0",  info: { title: "Tiny", version: "1.0.0" },  servers: [{ url: "https://httpbin.org" }],  paths: {    "/get": {      get: {        operationId: "httpGet",        summary: "GET",        responses: { "200": { description: "ok" } },      },    },  },}); const NEEDLE = "PLAINTEXT_NEEDLE_9f3a"; test("a connection value is stored encrypted at rest by the 'encrypted' provider", async () => {  const created = await Effect.runPromise(    Effect.gen(function* () {      const admin = yield* createScopedExecutor("admin", "default-org", "Default");      yield* admin.openapi.addSpec({        spec: { kind: "blob", value: TINY_SPEC },        slug: "tiny",        baseUrl: "",      });      // The connection's inline `value` is opaque to core (D11) — it is written      // through the default writable provider regardless of the template slug.      return yield* admin.connections.create({        owner: "org",        name: ConnectionName.make("gh"),        integration: IntegrationSlug.make("tiny"),        template: AuthTemplateSlug.make("bearer"),        value: NEEDLE,      });    }).pipe(Effect.provide(dbLayer), Effect.scoped),  );   // The first writable provider is the encrypted one — it handled the write.  expect(String(created.provider)).toBe("encrypted");   // Inspect the real SQLite file through a SEPARATE libSQL connection: the  // plaintext must NOT appear anywhere, and a versioned AES-GCM payload ("v1.")  // must be present. Reading through an independent connection also exercises the  // cross-connection visibility of FumaDB's writes.  const db = createClient({ url: `file:${dbPath}` });  const tables = (await db.execute("SELECT name FROM sqlite_master WHERE type='table'")).rows.map(    // oxlint-disable-next-line executor/no-redundant-primitive-cast -- boundary: sqlite_master.name is TEXT; narrow libSQL's SQLValue to string for the table list    (r) => r.name as string,  );  const cells: string[] = [];  for (const name of tables) {    const rows = (await db.execute(`SELECT * FROM "${name}"`)).rows;    for (const row of rows) {      for (const value of Object.values(row)) {        // Plugin-storage data is a BLOB (libSQL returns ArrayBuffer); decode it.        if (typeof value === "string") cells.push(value);        else if (value instanceof ArrayBuffer) cells.push(Buffer.from(value).toString("utf8"));        else if (ArrayBuffer.isView(value))          cells.push(            Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("utf8"),          );      }    }  }  db.close();   expect(cells.some((c) => c.includes(NEEDLE))).toBe(false);  expect(cells.some((c) => c.includes("v1."))).toBe(true);});