feat(FN-3766): add cli press sqlite store and credential helpers

Implements SQLite-based storage and credential management for the CLI printing press plugin, including type definitions, a persistent `cli-press-store`, and credential helpers, with updated routes, tests, and documentation.

Fusion-Task-Id: FN-3766
This commit is contained in:
Fusion
2026-05-10 18:11:03 -07:00
committed by gsxdsm
parent f7450ba3a2
commit 5a1518acd8
11 changed files with 1004 additions and 98 deletions

View File

@@ -1,79 +1,56 @@
# fusion-plugin-cli-printing-press
Bundled first-party Fusion plugin that adds a plugin-owned dashboard wizard for drafting an external service CLI definition.
Bundled first-party Fusion plugin for generating and managing service CLIs.
## v1 scope (FN-3763 + FN-3764 + FN-3765)
## Storage & Config Model
- Provides two dashboard views:
- **Create Service CLI** (`viewId: wizard`)
- **Manage Service CLIs** (`viewId: manage`)
- Wizard collects service basics, HTTP transport details, endpoints, and non-OAuth credential placeholders
- Manage view supports list/inspect/edit/regenerate/delete against saved drafts
- Saves draft payloads to interim JSON files under:
- `<projectRoot>/.fusion/plugins/cli-printing-press/drafts/<id>.json`
### Tables
## Run / Test panel (FN-3765)
- `cli_press_services`: service metadata (`id`, `slug`, `displayName`, `description`, `baseUrl`, `sourceKind`, `sourceRef`, timestamps)
- `cli_press_cli_specs`: generated/spec inputs per service (`id`, `serviceId`, `name`, `version`, `generatorVersion`, `specJson`, `generatedAt`, `status`, `lastGenerationError`, timestamps)
- `cli_press_artifacts`: generated artifact metadata (`id`, `cliSpecId`, `kind`, `path`, `executable`, `checksum`, `sizeBytes`, timestamps)
- `cli_press_credentials`: non-OAuth credentials (`id`, `serviceId`, `name`, `kind`, `value` envelope, `placement`, timestamps)
- `cli_press_service_settings`: service-scoped key/value settings (`id`, `serviceId`, `key`, `value`, `scope`, timestamps)
Each draft in **Manage Service CLIs** includes a **Run / Test** panel:
All IDs are UUIDv4-based with prefixes: `svc_`, `cli_`, `art_`, `cred_`, `set_`. Timestamps are ISO-8601 strings.
1. Click **Regenerate** to build or refresh a generated CLI artifact for the selected draft.
2. Pick an endpoint and fill endpoint parameters.
3. Provide credential values in transient password fields.
4. Click **Run** to execute the generated CLI against the configured service.
### Exported Types
The panel shows:
- status, duration, and redacted argv echo
- stdout and stderr
- exitCode and timeout state (`timedOut`)
- `Service`: canonical external-service record
- `CliSpec`: persisted cli-printing-press spec/generation state
- `CliArtifact`: artifact file metadata (path stored relative to `<projectRoot>/.fusion/`)
- `Credential`: persisted secret envelope + placement metadata
- `CredentialKind`: closed union of non-OAuth kinds (`api_key`, `bearer_token`, `basic_auth`, `header`, `query_param`, `env_var`)
- `CredentialPlacement`: discriminated placement union
- `ServiceSetting`: service-level setting entry (`runtime` | `wizard` | `metadata`)
- `OAuthNotSupportedError`: thrown when oauth/oauth2 is passed
- `InvalidCredentialPlacementError`: thrown on kind/placement mismatch or invalid `api_key` placement
### Credential contract
### Credential placement union
- Credentials are passed to the generated CLI via environment variables named:
- `CLIPP_CRED_<UPPER_SNAKE_KEY>`
- Credential values are **not persisted** in plugin draft files.
- Credential values are redacted from stdout, stderr, and argv echoes before API responses are returned.
- `{ kind: "header", header: string }`
- `{ kind: "query_param", queryParam: string }`
- `{ kind: "env_var", envVar: string }`
- `{ kind: "bearer_token", header: string }`
- `{ kind: "api_key", header?: string, queryParam?: string }` (exactly one required)
- `{ kind: "basic_auth", header: string }`
## Plugin API routes
### Credential encoding/materialization
Plugin views call host-prefixed plugin routes under `/api/plugins/cli-printing-press/`:
- Values are stored as `{ encoding: "base64", value: string }` via `encodeCredentialValue`/`decodeCredentialValue`.
- `applyCredentialToRequest` materializes credentials into `{ headers, query, env }` and rejects OAuth at runtime.
- `POST /drafts` — save draft
- `GET /drafts` — list summaries
- `GET /drafts/:id` — fetch full draft
- `PUT /drafts/:id` — update draft
- `DELETE /drafts/:id` — remove draft
- `POST /drafts/:id/regenerate` — generate/update CLI artifact and persist artifact metadata
- `GET /drafts/:id/artifact` — fetch artifact metadata
- `POST /drafts/:id/run` — run generated CLI and return run result
### OAuth policy (deferred)
### Run endpoint behavior
OAuth/OAuth2 flows are intentionally excluded from v1. Any `oauth`/`oauth2` kind is rejected by store-layer and helper-layer guards with `OAuthNotSupportedError`. Follow-up remains tracked in **FN-3762**.
`POST /drafts/:id/run` returns HTTP 200 for completed runs (including non-zero exits and timeouts), with outcome encoded in payload fields:
- `exitCode`
- `timedOut`
- `stdout`
- `stderr`
- `argv` (redacted)
### Artifact path convention
Validation and state errors return:
- `400` invalid body/params/timeout
- `404` unknown draft id
- `409` draft exists but has not been generated yet
Generated artifacts are expected under:
`<projectRoot>/.fusion/plugins/cli-printing-press/artifacts/<serviceId>/<specId>/<artifactFile>`
## Provisional architecture assumptions (pending FN-3762/FN-3766)
`CliArtifact.path` stores the path relative to `<projectRoot>/.fusion/`.
The following choices are intentionally provisional and may be revised by architecture/storage follow-up work:
### Deletions and filesystem cleanup
- `PluginContext` usage pattern in route handlers
- Express route shape and plugin-relative path conventions
- Credential union shape for wizard payloads (non-OAuth only in v1)
- Draft storage location and JSON schema
- Generator implementation details (kept behind `generateCli(...)` abstraction)
## Deferred follow-ups
- OAuth credential flows: **FN-3762 / FN-3766**
- Canonical storage migration (replace JSON stash): **FN-3766**
- Runtime exposure/integration: **FN-3767**
- Workflow-step exposure: **FN-3768**
- Persistent run history: deferred (response-only run results in FN-3765)
`deleteService`, `deleteSpec`, and `deleteArtifact` remove DB records. v1 intentionally does **not** remove artifact files from disk; cleanup is deferred to **FN-3767**.

View File

@@ -3,6 +3,7 @@ import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { Database } from "@fusion/core";
import { createCliPrintingPressRoutes } from "../routes/wizard-routes.js";
import type { ServiceDraft } from "../wizard/types.js";
@@ -49,7 +50,9 @@ describe("run routes", () => {
res.end("pong");
});
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-run-routes-"));
const ctx = { taskStore: { getRootDir: () => rootDir } } as any;
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const ctx = { taskStore: { getRootDir: () => rootDir, getDatabase: () => db } } as any;
const createRes = await route("POST", "/drafts").handler({ params: {}, body: makeDraft(baseUrl) }, ctx);
const id = (createRes.body as { id: string }).id;
@@ -71,7 +74,9 @@ describe("run routes", () => {
}, 50);
});
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-run-routes-"));
const ctx = { taskStore: { getRootDir: () => rootDir } } as any;
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const ctx = { taskStore: { getRootDir: () => rootDir, getDatabase: () => db } } as any;
const createRes = await route("POST", "/drafts").handler({ params: {}, body: makeDraft(baseUrl) }, ctx);
const id = (createRes.body as { id: string }).id;

View File

@@ -2,6 +2,7 @@ import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { Database } from "@fusion/core";
import { createCliPrintingPressRoutes } from "../routes/wizard-routes";
import type { ServiceDraft } from "../wizard/types";
@@ -19,7 +20,9 @@ function route(method: string, path: string) {
describe("wizard routes", () => {
it("handles create/get/delete lifecycle", async () => {
const rootDir = await mkdtemp(join(tmpdir(), "cli-printing-press-routes-"));
const ctx = { taskStore: { getRootDir: () => rootDir } } as any;
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const ctx = { taskStore: { getRootDir: () => rootDir, getDatabase: () => db } } as any;
const createRes = await route("POST", "/drafts").handler({ params: {}, body: makeDraft() }, ctx);
expect(createRes.status).toBe(201);

View File

@@ -1,5 +1,6 @@
import { definePlugin } from "@fusion/plugin-sdk";
import { createCliPrintingPressRoutes } from "./routes/wizard-routes.js";
import { ensureCliPressSchema } from "./store/cli-press-store.js";
const plugin = definePlugin({
manifest: {
@@ -9,7 +10,9 @@ const plugin = definePlugin({
description: "Guided wizard for drafting external service CLI definitions",
},
state: "installed",
hooks: {},
hooks: {
onSchemaInit: ensureCliPressSchema,
},
routes: createCliPrintingPressRoutes(),
dashboardViews: [
{
@@ -35,3 +38,5 @@ export default plugin;
export { CliPrintingPressWizardView } from "./dashboard-view.js";
export { CliPrintingPressManageView } from "./manage-view.js";
export { CliPrintingPressTestRunner } from "./run/TestRunnerPanel.js";
export { createCliPressStore, ensureCliPressSchema } from "./store/cli-press-store.js";
export * from "./store/cli-press-types.js";

View File

@@ -2,7 +2,8 @@ import type { PluginContext, PluginRouteDefinition, PluginRouteResult } from "@f
import { generateCli } from "../generation/generator.js";
import { runGeneratedCli } from "../generation/runner.js";
import type { GeneratedCliArtifact, RunRequest } from "../generation/types.js";
import { createDraftStore, getArtifactDir, NotFoundError } from "../storage/draft-store.js";
import { createCliPressStore } from "../store/cli-press-store.js";
import type { CliSpec, Service } from "../store/cli-press-types.js";
import type { ServiceDraft } from "../wizard/types.js";
import { validateDraft } from "../wizard/validation.js";
@@ -25,6 +26,32 @@ function asArtifact(draft: ServiceDraft): GeneratedCliArtifact | null {
};
}
function getArtifactDir(id: string, projectRoot: string): string {
return `${projectRoot}/.fusion/plugins/cli-printing-press/artifacts/${id}`;
}
function getStore(ctx: PluginContext) {
return createCliPressStore(ctx.taskStore.getDatabase());
}
function toDraft(service: Service, spec: CliSpec | undefined, endpoints: ServiceDraft["endpoints"]): ServiceDraft {
return {
id: service.id,
name: service.displayName,
slug: service.slug,
description: service.description ?? "",
baseUrl: service.baseUrl,
transport: "http",
endpoints,
credential: { kind: "none" },
createdAt: service.createdAt,
updatedAt: service.updatedAt,
generatedAt: spec?.generatedAt,
regeneratedAt: spec?.generatedAt,
artifactPath: spec?.status === "generated" ? spec.specJson : undefined,
};
}
function isPrimitive(value: unknown): value is string | number | boolean {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
@@ -69,8 +96,27 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
const draft = request.body as ServiceDraft;
const result = validateDraft(draft);
if (!result.ok) return { status: 400, body: { error: Object.values(result.errors)[0] ?? "Validation failed", errors: result.errors } };
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const created = await store.create(draft);
const store = getStore(ctx);
const createdService = store.createService({
slug: draft.slug,
displayName: draft.name,
description: draft.description,
baseUrl: draft.baseUrl,
sourceKind: "manual",
sourceRef: undefined,
});
const createdSpec = store.createSpec({
serviceId: createdService.id,
name: `${draft.slug}-cli`,
version: "0.1.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify(draft),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
store.setSetting({ serviceId: createdService.id, key: "endpoints", value: JSON.stringify(draft.endpoints), scope: "wizard" });
const created = toDraft(createdService, createdSpec, draft.endpoints);
return ok(created, 201);
},
},
@@ -78,8 +124,14 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
method: "GET",
path: "/drafts",
handler: async (_req, ctx: PluginContext) => {
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
return ok(await store.list());
const store = getStore(ctx);
const drafts = store.listServices().map((service) => ({
id: service.id,
name: service.displayName,
slug: service.slug,
updatedAt: service.updatedAt,
}));
return ok(drafts);
},
},
{
@@ -87,9 +139,13 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
path: "/drafts/:id",
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const draft = await store.get(request.params.id);
return draft ? ok(draft) : ok({ error: "Draft not found" }, 404);
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const spec = store.listSpecs(service.id)[0];
const endpointsSetting = store.listSettings(service.id).find((entry) => entry.key === "endpoints" && entry.scope === "wizard");
const endpoints = endpointsSetting ? (JSON.parse(endpointsSetting.value) as ServiceDraft["endpoints"]) : [];
return ok(toDraft(service, spec, endpoints));
},
},
{
@@ -100,14 +156,30 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
const draft = request.body as ServiceDraft;
const result = validateDraft(draft);
if (!result.ok) return { status: 400, body: { error: Object.values(result.errors)[0] ?? "Validation failed", errors: result.errors } };
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
try {
const updated = await store.update(request.params.id, draft);
return ok(updated);
} catch (error) {
if (error instanceof NotFoundError) return ok({ error: "Draft not found" }, 404);
throw error;
}
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const updatedService = store.updateService(service.id, {
displayName: draft.name,
description: draft.description,
baseUrl: draft.baseUrl,
sourceKind: "manual",
});
const existingSpec = store.listSpecs(service.id)[0];
const updatedSpec = existingSpec
? store.updateSpec(existingSpec.id, { specJson: JSON.stringify(draft), status: "draft", lastGenerationError: undefined })
: store.createSpec({
serviceId: service.id,
name: `${draft.slug}-cli`,
version: "0.1.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify(draft),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
store.setSetting({ serviceId: service.id, key: "endpoints", value: JSON.stringify(draft.endpoints), scope: "wizard" });
return ok(toDraft(updatedService, updatedSpec, draft.endpoints));
},
},
{
@@ -116,17 +188,28 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const projectRoot = ctx.taskStore.getRootDir();
const store = createDraftStore({ rootDir: projectRoot });
const existing = await store.get(request.params.id);
if (!existing) return ok({ error: "Draft not found" }, 404);
const artifact = await generateCli({ draft: existing, outDir: getArtifactDir(existing.id, projectRoot) });
const draft = await store.update(request.params.id, {
regeneratedAt: artifact.generatedAt,
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const spec = store.listSpecs(service.id)[0];
if (!spec) return ok({ error: "Draft not found" }, 404);
const draft = JSON.parse(spec.specJson) as ServiceDraft;
const artifact = await generateCli({ draft, outDir: getArtifactDir(draft.id, projectRoot) });
const updatedSpec = store.updateSpec(spec.id, {
specJson: JSON.stringify({ ...draft, regeneratedAt: artifact.generatedAt, generatedAt: artifact.generatedAt, artifactPath: artifact.binPath }),
generatedAt: artifact.generatedAt,
artifactPath: artifact.binPath,
status: "generated",
lastGenerationError: undefined,
});
return ok({ draft, artifact });
store.createArtifact({
cliSpecId: spec.id,
kind: "script",
path: artifact.binPath.replace(`${projectRoot}/.fusion/`, ""),
executable: true,
checksum: undefined,
sizeBytes: undefined,
});
return ok({ draft: JSON.parse(updatedSpec.specJson) as ServiceDraft, artifact });
},
},
{
@@ -137,10 +220,12 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
const parsed = validateRunRequest(request.body);
if (!parsed.ok) return ok({ error: parsed.error }, 400);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const draft = await store.get(request.params.id);
if (!draft) return ok({ error: "Draft not found" }, 404);
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Draft not found" }, 404);
const spec = store.listSpecs(service.id)[0];
if (!spec) return ok({ error: "Draft not found" }, 404);
const draft = JSON.parse(spec.specJson) as ServiceDraft;
const artifact = asArtifact(draft);
if (!artifact) return ok({ error: "Draft has not been generated yet" }, 409);
@@ -163,9 +248,13 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
path: "/drafts/:id/artifact",
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
const draft = await store.get(request.params.id);
const artifact = draft ? asArtifact(draft) : null;
const store = getStore(ctx);
const service = store.getService(request.params.id);
if (!service) return ok({ error: "Artifact not found" }, 404);
const spec = store.listSpecs(service.id)[0];
if (!spec) return ok({ error: "Artifact not found" }, 404);
const draft = JSON.parse(spec.specJson) as ServiceDraft;
const artifact = asArtifact(draft);
return artifact ? ok({ artifact }) : ok({ error: "Artifact not found" }, 404);
},
},
@@ -174,8 +263,8 @@ export function createCliPrintingPressRoutes(): PluginRouteDefinition[] {
path: "/drafts/:id",
handler: async (req, ctx: PluginContext) => {
const request = asRequest(req);
const store = createDraftStore({ rootDir: ctx.taskStore.getRootDir() });
await store.delete(request.params.id);
const store = getStore(ctx);
store.deleteService(request.params.id);
return { status: 204 };
},
},

View File

@@ -0,0 +1,128 @@
import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "@fusion/core";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createCliPressStore, ensureCliPressSchema } from "../cli-press-store.js";
import { encodeCredentialValue } from "../credentials.js";
describe("cli-press-store", () => {
let rootDir: string;
let db: Database;
let store: ReturnType<typeof createCliPressStore>;
beforeEach(() => {
rootDir = mkdtempSync(join(tmpdir(), "cli-press-store-"));
db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
ensureCliPressSchema(db);
ensureCliPressSchema(db);
store = createCliPressStore(db);
});
afterEach(async () => {
db.close();
await rm(rootDir, { recursive: true, force: true });
});
it("creates schema idempotently and runs full CRUD", () => {
const service = store.createService({
slug: "demo",
displayName: "Demo",
description: "Demo service",
baseUrl: "https://example.com",
sourceKind: "manual",
sourceRef: "wizard",
});
expect(service.id).toMatch(/^svc_/);
const updatedService = store.updateService(service.id, { displayName: "Demo Updated" });
expect(updatedService.displayName).toBe("Demo Updated");
const spec = store.createSpec({
serviceId: service.id,
name: "demo-cli",
version: "0.1.0",
generatorVersion: "1.0.0",
specJson: JSON.stringify({ hello: "world" }),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
expect(store.getSpec(spec.id)?.specJson).toBe(JSON.stringify({ hello: "world" }));
const updatedSpec = store.updateSpec(spec.id, { status: "generated" });
expect(updatedSpec.status).toBe("generated");
const artifact = store.createArtifact({
cliSpecId: spec.id,
kind: "script",
path: "plugins/cli-printing-press/artifacts/demo.sh",
executable: true,
checksum: "abc",
sizeBytes: 42,
});
expect(artifact.id).toMatch(/^art_/);
const cred = store.createCredential({
serviceId: service.id,
name: "api",
kind: "api_key",
value: encodeCredentialValue("secret"),
placement: { kind: "api_key", header: "X-API-Key" },
});
expect(cred.id).toMatch(/^cred_/);
const setting = store.setSetting({
serviceId: service.id,
key: "region",
value: "us-east-1",
scope: "runtime",
});
expect(setting.id).toMatch(/^set_/);
expect(store.listServices()).toHaveLength(1);
expect(store.listSpecs(service.id)).toHaveLength(1);
expect(store.listArtifacts(spec.id)).toHaveLength(1);
expect(store.listCredentials(service.id)).toHaveLength(1);
expect(store.listSettings(service.id)).toHaveLength(1);
store.deleteService(service.id);
expect(store.listServices()).toHaveLength(0);
expect(store.listSpecs(service.id)).toHaveLength(0);
expect(store.listCredentials(service.id)).toHaveLength(0);
expect(store.listSettings(service.id)).toHaveLength(0);
});
it("rejects oauth and invalid placement", () => {
const service = store.createService({
slug: "oauth-demo",
displayName: "OAuth Demo",
description: "",
baseUrl: "https://example.com",
sourceKind: "manual",
sourceRef: "wizard",
});
expect(() =>
store.createCredential({
serviceId: service.id,
name: "bad",
kind: "oauth" as never,
value: encodeCredentialValue("x"),
placement: { kind: "header", header: "Authorization" },
}),
).toThrow("not supported");
expect(() =>
store.createCredential({
serviceId: service.id,
name: "bad2",
kind: "api_key",
value: encodeCredentialValue("x"),
placement: { kind: "api_key", header: "X", queryParam: "token" },
}),
).toThrow("Invalid credential placement");
});
});

View File

@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import type { Credential } from "../cli-press-types.js";
import { applyCredentialToRequest, decodeCredentialValue, encodeCredentialValue } from "../credentials.js";
function baseCredential(partial: Partial<Credential>): Credential {
return {
id: "cred_1",
serviceId: "svc_1",
name: "cred",
kind: "header",
value: encodeCredentialValue("secret"),
placement: { kind: "header", header: "X-Custom-Token" },
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...partial,
} as Credential;
}
describe("credentials", () => {
it("encodes and decodes", () => {
const encoded = encodeCredentialValue("alice:s3cret");
expect(encoded.encoding).toBe("base64");
expect(decodeCredentialValue(encoded)).toBe("alice:s3cret");
});
it("applies all credential kinds", () => {
const request = { headers: {}, query: {}, env: {} };
applyCredentialToRequest(baseCredential({ kind: "header", placement: { kind: "header", header: "X-Custom-Token" } }), request);
applyCredentialToRequest(baseCredential({ kind: "query_param", placement: { kind: "query_param", queryParam: "api_token" } }), request);
applyCredentialToRequest(baseCredential({ kind: "env_var", placement: { kind: "env_var", envVar: "GITHUB_TOKEN" } }), request);
applyCredentialToRequest(baseCredential({ kind: "bearer_token", placement: { kind: "bearer_token", header: "Authorization" } }), request);
applyCredentialToRequest(baseCredential({ kind: "api_key", placement: { kind: "api_key", header: "X-API-Key" } }), request);
applyCredentialToRequest(baseCredential({ kind: "api_key", placement: { kind: "api_key", queryParam: "api_key" } }), request);
applyCredentialToRequest(
baseCredential({
kind: "basic_auth",
value: encodeCredentialValue("alice:s3cret"),
placement: { kind: "basic_auth", header: "Authorization" },
}),
request,
);
expect(request.headers["X-Custom-Token"]).toBe("secret");
expect(request.query.api_token).toBe("secret");
expect(request.env.GITHUB_TOKEN).toBe("secret");
expect(request.headers.Authorization).toBe("Basic YWxpY2U6czNjcmV0");
expect(request.headers["X-API-Key"]).toBe("secret");
expect(request.query.api_key).toBe("secret");
});
it("throws on placement mismatch and oauth", () => {
const request = { headers: {}, query: {}, env: {} };
expect(() =>
applyCredentialToRequest(baseCredential({ kind: "header", placement: { kind: "query_param", queryParam: "x" } as any }), request),
).toThrow("Invalid credential placement");
expect(() =>
applyCredentialToRequest(baseCredential({ kind: "oauth" as any, placement: { kind: "oauth", header: "Authorization" } as any }), request),
).toThrow("not supported");
});
});

View File

@@ -0,0 +1,427 @@
import { randomUUID } from "node:crypto";
import type { Database } from "@fusion/core";
import {
InvalidCredentialPlacementError,
OAuthNotSupportedError,
type CliArtifact,
type CliArtifactCreateInput,
type CliArtifactUpdateInput,
type CliSpec,
type CliSpecCreateInput,
type CliSpecUpdateInput,
type Credential,
type CredentialCreateInput,
type CredentialUpdateInput,
type Service,
type ServiceCreateInput,
type ServiceSetting,
type ServiceSettingCreateInput,
type ServiceUpdateInput,
} from "./cli-press-types.js";
interface ServiceRow {
id: string;
slug: string;
displayName: string;
description: string | null;
baseUrl: string;
sourceKind: Service["sourceKind"];
sourceRef: string | null;
createdAt: string;
updatedAt: string;
}
interface CliSpecRow {
id: string;
serviceId: string;
name: string;
version: string;
generatorVersion: string;
specJson: string;
generatedAt: string | null;
status: CliSpec["status"];
lastGenerationError: string | null;
createdAt: string;
updatedAt: string;
}
interface CliArtifactRow {
id: string;
cliSpecId: string;
kind: CliArtifact["kind"];
path: string;
executable: number;
checksum: string | null;
sizeBytes: number | null;
createdAt: string;
updatedAt: string;
}
interface CredentialRow {
id: string;
serviceId: string;
name: string;
kind: Credential["kind"];
value: string;
placement: string;
createdAt: string;
updatedAt: string;
}
interface ServiceSettingRow {
id: string;
serviceId: string;
key: string;
value: string;
scope: ServiceSetting["scope"];
createdAt: string;
updatedAt: string;
}
const OAUTH_KINDS = new Set(["oauth", "oauth2"]);
function parseJson<T>(value: string): T {
return JSON.parse(value) as T;
}
function nowIso(): string {
return new Date().toISOString();
}
function createId(prefix: "svc" | "cli" | "art" | "cred" | "set"): string {
return `${prefix}_${randomUUID()}`;
}
function assertCredentialSupported(kind: string): void {
if (OAUTH_KINDS.has(kind)) {
throw new OAuthNotSupportedError(kind);
}
}
function assertPlacementConsistency(
kind: Credential["kind"] | string,
placement: Credential["placement"] | { kind?: string; header?: string; queryParam?: string },
): void {
const placementKind = (placement as { kind?: string }).kind;
if (placementKind !== kind) {
throw new InvalidCredentialPlacementError({ credentialKind: kind, placementKind: String(placementKind) });
}
if (kind === "api_key") {
const candidate = placement as { kind?: string; header?: string; queryParam?: string };
const hasHeader = typeof candidate.header === "string" && candidate.header.trim().length > 0;
const hasQuery = typeof candidate.queryParam === "string" && candidate.queryParam.trim().length > 0;
if ((hasHeader ? 1 : 0) + (hasQuery ? 1 : 0) !== 1) {
throw new InvalidCredentialPlacementError({ credentialKind: kind, placementKind: String(placementKind) });
}
}
}
export function ensureCliPressSchema(db: Database): void {
db.exec(`
CREATE TABLE IF NOT EXISTS cli_press_services (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
displayName TEXT NOT NULL,
description TEXT,
baseUrl TEXT NOT NULL,
sourceKind TEXT NOT NULL,
sourceRef TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS cli_press_cli_specs (
id TEXT PRIMARY KEY,
serviceId TEXT NOT NULL,
name TEXT NOT NULL,
version TEXT NOT NULL,
generatorVersion TEXT NOT NULL,
specJson TEXT NOT NULL,
generatedAt TEXT,
status TEXT NOT NULL,
lastGenerationError TEXT,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (serviceId) REFERENCES cli_press_services(id) ON DELETE CASCADE,
UNIQUE(serviceId, name)
);
CREATE TABLE IF NOT EXISTS cli_press_artifacts (
id TEXT PRIMARY KEY,
cliSpecId TEXT NOT NULL,
kind TEXT NOT NULL,
path TEXT NOT NULL,
executable INTEGER NOT NULL,
checksum TEXT,
sizeBytes INTEGER,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (cliSpecId) REFERENCES cli_press_cli_specs(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS cli_press_credentials (
id TEXT PRIMARY KEY,
serviceId TEXT NOT NULL,
name TEXT NOT NULL,
kind TEXT NOT NULL,
value TEXT NOT NULL,
placement TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (serviceId) REFERENCES cli_press_services(id) ON DELETE CASCADE,
UNIQUE(serviceId, name)
);
CREATE TABLE IF NOT EXISTS cli_press_service_settings (
id TEXT PRIMARY KEY,
serviceId TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
scope TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
FOREIGN KEY (serviceId) REFERENCES cli_press_services(id) ON DELETE CASCADE,
UNIQUE(serviceId, key, scope)
);
CREATE INDEX IF NOT EXISTS idx_cli_press_specs_service ON cli_press_cli_specs(serviceId);
CREATE INDEX IF NOT EXISTS idx_cli_press_artifacts_spec ON cli_press_artifacts(cliSpecId);
CREATE INDEX IF NOT EXISTS idx_cli_press_credentials_service ON cli_press_credentials(serviceId);
CREATE INDEX IF NOT EXISTS idx_cli_press_settings_service ON cli_press_service_settings(serviceId);
`);
}
export function createCliPressStore(db: Database) {
ensureCliPressSchema(db);
const mapService = (row: ServiceRow): Service => ({
id: row.id,
slug: row.slug,
displayName: row.displayName,
description: row.description ?? undefined,
baseUrl: row.baseUrl,
sourceKind: row.sourceKind,
sourceRef: row.sourceRef ?? undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapSpec = (row: CliSpecRow): CliSpec => ({
id: row.id,
serviceId: row.serviceId,
name: row.name,
version: row.version,
generatorVersion: row.generatorVersion,
specJson: row.specJson,
generatedAt: row.generatedAt ?? undefined,
status: row.status,
lastGenerationError: row.lastGenerationError ?? undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapArtifact = (row: CliArtifactRow): CliArtifact => ({
id: row.id,
cliSpecId: row.cliSpecId,
kind: row.kind,
path: row.path,
executable: Boolean(row.executable),
checksum: row.checksum ?? undefined,
sizeBytes: row.sizeBytes ?? undefined,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapCredential = (row: CredentialRow): Credential => ({
id: row.id,
serviceId: row.serviceId,
name: row.name,
kind: row.kind,
value: parseJson(row.value),
placement: parseJson(row.placement),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
const mapSetting = (row: ServiceSettingRow): ServiceSetting => ({
id: row.id,
serviceId: row.serviceId,
key: row.key,
value: row.value,
scope: row.scope,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
return {
listServices(): Service[] {
const rows = db.prepare("SELECT * FROM cli_press_services ORDER BY createdAt DESC").all() as unknown as ServiceRow[];
return rows.map(mapService);
},
getService(id: string): Service | undefined {
const row = db.prepare("SELECT * FROM cli_press_services WHERE id = ?").get(id) as unknown as ServiceRow | undefined;
return row ? mapService(row) : undefined;
},
createService(input: ServiceCreateInput): Service {
const service: Service = { id: createId("svc"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_services (id, slug, displayName, description, baseUrl, sourceKind, sourceRef, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(service.id, service.slug, service.displayName, service.description ?? null, service.baseUrl, service.sourceKind, service.sourceRef ?? null, service.createdAt, service.updatedAt);
db.bumpLastModified();
return service;
},
updateService(id: string, updates: ServiceUpdateInput): Service {
const existing = this.getService(id);
if (!existing) throw new Error(`Service ${id} not found`);
const updated: Service = { ...existing, ...updates, id: existing.id, slug: existing.slug, createdAt: existing.createdAt, updatedAt: nowIso() };
db.prepare(`UPDATE cli_press_services SET displayName = ?, description = ?, baseUrl = ?, sourceKind = ?, sourceRef = ?, updatedAt = ? WHERE id = ?`)
.run(updated.displayName, updated.description ?? null, updated.baseUrl, updated.sourceKind, updated.sourceRef ?? null, updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteService(id: string): void {
db.transaction(() => {
db.prepare("DELETE FROM cli_press_services WHERE id = ?").run(id);
});
db.bumpLastModified();
},
listSpecs(serviceId: string): CliSpec[] {
const rows = db.prepare("SELECT * FROM cli_press_cli_specs WHERE serviceId = ? ORDER BY createdAt DESC").all(serviceId) as unknown as CliSpecRow[];
return rows.map(mapSpec);
},
getSpec(id: string): CliSpec | undefined {
const row = db.prepare("SELECT * FROM cli_press_cli_specs WHERE id = ?").get(id) as unknown as CliSpecRow | undefined;
return row ? mapSpec(row) : undefined;
},
createSpec(input: CliSpecCreateInput): CliSpec {
const spec: CliSpec = { id: createId("cli"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_cli_specs (id, serviceId, name, version, generatorVersion, specJson, generatedAt, status, lastGenerationError, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(spec.id, spec.serviceId, spec.name, spec.version, spec.generatorVersion, spec.specJson, spec.generatedAt ?? null, spec.status, spec.lastGenerationError ?? null, spec.createdAt, spec.updatedAt);
db.bumpLastModified();
return spec;
},
updateSpec(id: string, updates: CliSpecUpdateInput): CliSpec {
const existing = this.getSpec(id);
if (!existing) throw new Error(`Spec ${id} not found`);
const updated: CliSpec = { ...existing, ...updates, id: existing.id, serviceId: existing.serviceId, createdAt: existing.createdAt, updatedAt: nowIso() };
db.prepare(`UPDATE cli_press_cli_specs SET name=?, version=?, generatorVersion=?, specJson=?, generatedAt=?, status=?, lastGenerationError=?, updatedAt=? WHERE id=?`)
.run(updated.name, updated.version, updated.generatorVersion, updated.specJson, updated.generatedAt ?? null, updated.status, updated.lastGenerationError ?? null, updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteSpec(id: string): void {
db.prepare("DELETE FROM cli_press_cli_specs WHERE id = ?").run(id);
db.bumpLastModified();
},
listArtifacts(specId: string): CliArtifact[] {
const rows = db.prepare("SELECT * FROM cli_press_artifacts WHERE cliSpecId = ? ORDER BY createdAt DESC").all(specId) as unknown as CliArtifactRow[];
return rows.map(mapArtifact);
},
createArtifact(input: CliArtifactCreateInput): CliArtifact {
const artifact: CliArtifact = { id: createId("art"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_artifacts (id, cliSpecId, kind, path, executable, checksum, sizeBytes, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(artifact.id, artifact.cliSpecId, artifact.kind, artifact.path, artifact.executable ? 1 : 0, artifact.checksum ?? null, artifact.sizeBytes ?? null, artifact.createdAt, artifact.updatedAt);
db.bumpLastModified();
return artifact;
},
updateArtifact(id: string, updates: CliArtifactUpdateInput): CliArtifact {
const existing = db.prepare("SELECT * FROM cli_press_artifacts WHERE id = ?").get(id) as unknown as CliArtifactRow | undefined;
if (!existing) throw new Error(`Artifact ${id} not found`);
const updated = { ...mapArtifact(existing), ...updates, id: existing.id, cliSpecId: existing.cliSpecId, createdAt: existing.createdAt, updatedAt: nowIso() };
db.prepare("UPDATE cli_press_artifacts SET path=?, executable=?, checksum=?, sizeBytes=?, updatedAt=? WHERE id=?")
.run(updated.path, updated.executable ? 1 : 0, updated.checksum ?? null, updated.sizeBytes ?? null, updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteArtifact(id: string): void {
db.prepare("DELETE FROM cli_press_artifacts WHERE id = ?").run(id);
db.bumpLastModified();
},
listCredentials(serviceId: string): Credential[] {
const rows = db.prepare("SELECT * FROM cli_press_credentials WHERE serviceId = ? ORDER BY createdAt DESC").all(serviceId) as unknown as CredentialRow[];
return rows.map(mapCredential);
},
createCredential(input: CredentialCreateInput): Credential {
assertCredentialSupported(input.kind);
assertPlacementConsistency(input.kind, input.placement);
const cred: Credential = { id: createId("cred"), ...input, createdAt: nowIso(), updatedAt: nowIso() };
db.prepare(`INSERT INTO cli_press_credentials (id, serviceId, name, kind, value, placement, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
.run(cred.id, cred.serviceId, cred.name, cred.kind, JSON.stringify(cred.value), JSON.stringify(cred.placement), cred.createdAt, cred.updatedAt);
db.bumpLastModified();
return cred;
},
updateCredential(id: string, updates: CredentialUpdateInput): Credential {
const existing = db.prepare("SELECT * FROM cli_press_credentials WHERE id = ?").get(id) as unknown as CredentialRow | undefined;
if (!existing) throw new Error(`Credential ${id} not found`);
const mapped = mapCredential(existing);
const updated: Credential = {
...mapped,
...updates,
id: mapped.id,
serviceId: mapped.serviceId,
kind: mapped.kind,
createdAt: mapped.createdAt,
updatedAt: nowIso(),
};
assertCredentialSupported(updated.kind);
assertPlacementConsistency(updated.kind, updated.placement);
db.prepare("UPDATE cli_press_credentials SET name=?, value=?, placement=?, updatedAt=? WHERE id=?")
.run(updated.name, JSON.stringify(updated.value), JSON.stringify(updated.placement), updated.updatedAt, id);
db.bumpLastModified();
return updated;
},
deleteCredential(id: string): void {
db.prepare("DELETE FROM cli_press_credentials WHERE id = ?").run(id);
db.bumpLastModified();
},
listSettings(serviceId: string): ServiceSetting[] {
const rows = db.prepare("SELECT * FROM cli_press_service_settings WHERE serviceId = ? ORDER BY createdAt DESC").all(serviceId) as unknown as ServiceSettingRow[];
return rows.map(mapSetting);
},
setSetting(input: ServiceSettingCreateInput): ServiceSetting {
const existing = db.prepare("SELECT * FROM cli_press_service_settings WHERE serviceId = ? AND key = ? AND scope = ?")
.get(input.serviceId, input.key, input.scope) as unknown as ServiceSettingRow | undefined;
const now = nowIso();
if (existing) {
db.prepare("UPDATE cli_press_service_settings SET value = ?, updatedAt = ? WHERE id = ?").run(input.value, now, existing.id);
db.bumpLastModified();
return mapSetting({ ...existing, value: input.value, updatedAt: now });
}
const setting: ServiceSetting = { id: createId("set"), ...input, createdAt: now, updatedAt: now };
db.prepare(`INSERT INTO cli_press_service_settings (id, serviceId, key, value, scope, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
.run(setting.id, setting.serviceId, setting.key, setting.value, setting.scope, setting.createdAt, setting.updatedAt);
db.bumpLastModified();
return setting;
},
deleteSetting(id: string): void {
db.prepare("DELETE FROM cli_press_service_settings WHERE id = ?").run(id);
db.bumpLastModified();
},
};
}
export type CliPressStore = ReturnType<typeof createCliPressStore>;

View File

@@ -0,0 +1,110 @@
export type ServiceSourceKind = "openapi" | "manual" | "other";
export type CredentialKind = "api_key" | "bearer_token" | "basic_auth" | "header" | "query_param" | "env_var";
export type CredentialPlacement =
| { kind: "header"; header: string }
| { kind: "query_param"; queryParam: string }
| { kind: "env_var"; envVar: string }
| { kind: "bearer_token"; header: string }
| { kind: "api_key"; header?: string; queryParam?: string }
| { kind: "basic_auth"; header: string };
export type ServiceSettingScope = "runtime" | "wizard" | "metadata";
export interface Service {
id: string;
slug: string;
displayName: string;
description?: string;
baseUrl: string;
sourceKind: ServiceSourceKind;
sourceRef?: string;
createdAt: string;
updatedAt: string;
}
export interface CliSpec {
id: string;
serviceId: string;
name: string;
version: string;
generatorVersion: string;
specJson: string;
generatedAt?: string;
status: "draft" | "generated" | "failed";
lastGenerationError?: string;
createdAt: string;
updatedAt: string;
}
export interface CliArtifact {
id: string;
cliSpecId: string;
kind: "binary" | "script" | "package";
path: string;
executable: boolean;
checksum?: string;
sizeBytes?: number;
createdAt: string;
updatedAt: string;
}
export interface EncodedCredentialValue {
encoding: "base64";
value: string;
}
export interface Credential {
id: string;
serviceId: string;
name: string;
kind: CredentialKind;
value: EncodedCredentialValue;
placement: CredentialPlacement;
createdAt: string;
updatedAt: string;
}
export interface ServiceSetting {
id: string;
serviceId: string;
key: string;
value: string;
scope: ServiceSettingScope;
createdAt: string;
updatedAt: string;
}
export class OAuthNotSupportedError extends Error {
constructor(kind: string) {
super(`Credential kind \"${kind}\" is not supported. OAuth/OAuth2 are deferred.`);
this.name = "OAuthNotSupportedError";
}
}
export class InvalidCredentialPlacementError extends Error {
constructor(input: { credentialKind: string; placementKind: string }) {
super(
`Invalid credential placement: credential kind \"${input.credentialKind}\" does not match placement kind \"${input.placementKind}\".`,
);
this.name = "InvalidCredentialPlacementError";
}
}
export type ServiceCreateInput = Omit<Service, "id" | "createdAt" | "updatedAt">;
export type ServiceUpdateInput = Partial<Pick<Service, "displayName" | "description" | "baseUrl" | "sourceKind" | "sourceRef">>;
export type CliSpecCreateInput = Omit<CliSpec, "id" | "createdAt" | "updatedAt">;
export type CliSpecUpdateInput = Partial<
Pick<CliSpec, "name" | "version" | "generatorVersion" | "specJson" | "status" | "lastGenerationError" | "generatedAt">
>;
export type CliArtifactCreateInput = Omit<CliArtifact, "id" | "createdAt" | "updatedAt">;
export type CliArtifactUpdateInput = Partial<Pick<CliArtifact, "path" | "executable" | "checksum" | "sizeBytes">>;
export type CredentialCreateInput = Omit<Credential, "id" | "createdAt" | "updatedAt">;
export type CredentialUpdateInput = Partial<Pick<Credential, "name" | "value" | "placement">>;
export type ServiceSettingCreateInput = Omit<ServiceSetting, "id" | "createdAt" | "updatedAt">;
export type ServiceSettingUpdateInput = Pick<ServiceSetting, "value">;

View File

@@ -0,0 +1,95 @@
import { Buffer } from "node:buffer";
import type { Credential, EncodedCredentialValue } from "./cli-press-types.js";
import { InvalidCredentialPlacementError, OAuthNotSupportedError } from "./cli-press-types.js";
export type RequestShape = {
headers: Record<string, string>;
query: Record<string, string>;
env: Record<string, string>;
};
export function encodeCredentialValue(raw: string): EncodedCredentialValue {
return {
encoding: "base64",
value: Buffer.from(raw, "utf8").toString("base64"),
};
}
export function decodeCredentialValue(encoded: EncodedCredentialValue): string {
if (encoded.encoding !== "base64") {
throw new Error(`Unsupported credential encoding: ${String((encoded as { encoding?: string }).encoding)}`);
}
return Buffer.from(encoded.value, "base64").toString("utf8");
}
function assertNoOAuth(kind: string): void {
if (kind === "oauth" || kind === "oauth2") {
throw new OAuthNotSupportedError(kind);
}
}
function assertPlacement(credential: Credential): void {
const credentialKind = credential.kind;
const placementKind = credential.placement.kind;
if (placementKind !== credentialKind) {
throw new InvalidCredentialPlacementError({ credentialKind, placementKind });
}
if (credential.kind === "api_key") {
const placement = credential.placement;
if (placement.kind !== "api_key") {
throw new InvalidCredentialPlacementError({ credentialKind, placementKind });
}
const hasHeader = typeof placement.header === "string" && placement.header.trim().length > 0;
const hasQuery = typeof placement.queryParam === "string" && placement.queryParam.trim().length > 0;
if ((hasHeader ? 1 : 0) + (hasQuery ? 1 : 0) !== 1) {
throw new InvalidCredentialPlacementError({ credentialKind, placementKind });
}
}
}
export function applyCredentialToRequest(credential: Credential, request: RequestShape): RequestShape {
assertNoOAuth((credential as { kind: string }).kind);
assertPlacement(credential);
const value = decodeCredentialValue(credential.value);
switch (credential.kind) {
case "header": {
const placement = credential.placement as { kind: "header"; header: string };
request.headers[placement.header] = value;
break;
}
case "query_param": {
const placement = credential.placement as { kind: "query_param"; queryParam: string };
request.query[placement.queryParam] = value;
break;
}
case "env_var": {
const placement = credential.placement as { kind: "env_var"; envVar: string };
request.env[placement.envVar] = value;
break;
}
case "bearer_token": {
const placement = credential.placement as { kind: "bearer_token"; header: string };
request.headers[placement.header] = `Bearer ${value}`;
break;
}
case "api_key": {
const placement = credential.placement as { kind: "api_key"; header?: string; queryParam?: string };
if (placement.header) {
request.headers[placement.header] = value;
} else if (placement.queryParam) {
request.query[placement.queryParam] = value;
}
break;
}
case "basic_auth": {
const placement = credential.placement as { kind: "basic_auth"; header: string };
const basicAuth = Buffer.from(value, "utf8").toString("base64");
request.headers[placement.header] = `Basic ${basicAuth}`;
break;
}
}
return request;
}