Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ const res = await client.proxy.request({
body: { user: "%USER_ID%" },
// Optional per-call overrides:
// environment: "other-env-id",
// usePersonal: false,
});
```

Expand Down
10 changes: 4 additions & 6 deletions src/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,10 @@ export class HttpInterceptor {
method,
headers: mergedHeaders,
body: finalBody,
config: {
workspace: rule.workspace ?? this.#defaults.workspace,
project: rule.project ?? this.#defaults.project,
"environment-id": rule.environment ?? this.#defaults.environment,
"is-personal": rule.usePersonal ?? this.#defaults.usePersonalValues,
},
workspace: rule.workspace ?? this.#defaults.workspace,
project: rule.project ?? this.#defaults.project,
"environment-id": rule.environment ?? this.#defaults.environment,
"is-personal": rule.usePersonal ?? this.#defaults.usePersonalValues,
};
}

Expand Down
30 changes: 17 additions & 13 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@ export interface ProxyWireBody {
method: ProxyMethod;
headers?: Record<string, string>;
body?: JsonValue;
config: {
workspace: string;
project: string;
"environment-id": string;
"is-personal": boolean;
};
workspace: string;
project: string;
"environment-id": string;
"is-personal": boolean;
}

/**
Expand Down Expand Up @@ -82,15 +80,16 @@ export async function sendProxyWire(
const wireBody: Record<string, unknown> = {
url: body.url,
method: body.method,
config: body.config,
"is-personal": body["is-personal"],
};
if (body.headers !== undefined) wireBody.headers = body.headers;
if (body.body !== undefined) wireBody.body = body.body;

const proxyRequestUrl = buildProxyRequestUrl(ctx.proxyUrl, body.workspace, body.project, body["environment-id"]);
ctx.logger.debug(`Proxy request: ${body.method} ${body.url}`);
const start = Date.now();

const response = await fetch(ctx.proxyUrl, {
const response = await fetch(proxyRequestUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand Down Expand Up @@ -179,7 +178,7 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
method,
headers,
body,
config: this.#buildConfig(),
...this.#buildScope(),
},
init?.signal ?? null,
);
Expand All @@ -202,7 +201,7 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
);
}

const config = this.#buildConfig({
const scope = this.#buildScope({
workspace: options.workspace,
project: options.project,
environment: options.environment,
Expand All @@ -216,18 +215,18 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
method,
headers: options.headers,
body: options.body,
config,
...scope,
},
null,
);
}

#buildConfig(overrides?: {
#buildScope(overrides?: {
workspace?: string;
project?: string;
environment?: string;
usePersonal?: boolean;
}): ProxyWireBody["config"] {
}): Pick<ProxyWireBody, "workspace" | "project" | "environment-id" | "is-personal"> {
return {
workspace: overrides?.workspace ?? this.#workspace,
project: overrides?.project ?? this.#project,
Expand Down Expand Up @@ -309,3 +308,8 @@ function bodyTypeError(typeName: string): EnkryptifyError {
"Docs: https://docs.enkryptify.com/sdk/proxy",
);
}

function buildProxyRequestUrl(baseUrl: string, workspace: string, project: string, environmentId: string): string {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
return `${normalizedBaseUrl}/${encodeURIComponent(workspace)}/${encodeURIComponent(project)}/${encodeURIComponent(environmentId)}`;
}
22 changes: 8 additions & 14 deletions tests/interceptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async function findProxyCall(fetchMock: ReturnType<typeof vi.fn>): Promise<Recor
if (!parsed) continue;
// mswjs sometimes normalises the URL with a trailing slash when it
// runs through URL(); accept both shapes.
if (parsed.url === "https://proxy.test.com" || parsed.url === "https://proxy.test.com/") {
if (parsed.url.startsWith("https://proxy.test.com/")) {
if (parsed.bodyText === null) return null;
try {
return JSON.parse(parsed.bodyText) as Record<string, unknown>;
Expand Down Expand Up @@ -359,7 +359,7 @@ describe("interceptor — rule matching", () => {
});

describe("interceptor — ProxyWireBody shape", () => {
it("includes config block with client defaults", async () => {
it("puts client default context in the proxy URL path", async () => {
fetchMock.mockResolvedValue(new Response("{}", { status: 200 }));

activeClient = new Enkryptify(
Expand All @@ -378,12 +378,9 @@ describe("interceptor — ProxyWireBody shape", () => {
await fetch("https://api.example.com/v1");

const wire = await findProxyCall(fetchMock);
expect(wire?.config).toEqual({
workspace: "ws-x",
project: "prj-y",
"environment-id": "env-z",
"is-personal": false,
});
expect(wire?.["is-personal"]).toBe(false);
const proxyCall = await findCallByUrlPrefix(fetchMock, "https://proxy.test.com/");
expect(proxyCall?.url).toBe("https://proxy.test.com/ws-x/prj-y/env-z");
});

it("rule-level workspace/project/environment/usePersonal override defaults", async () => {
Expand All @@ -410,12 +407,9 @@ describe("interceptor — ProxyWireBody shape", () => {
await fetch("https://api.example.com/v1");

const wire = await findProxyCall(fetchMock);
expect(wire?.config).toEqual({
workspace: "override-ws",
project: "override-prj",
"environment-id": "override-env",
"is-personal": false,
});
expect(wire?.["is-personal"]).toBe(false);
const proxyCall = await findCallByUrlPrefix(fetchMock, "https://proxy.test.com/");
expect(proxyCall?.url).toBe("https://proxy.test.com/override-ws/override-prj/override-env");
});

it("sends Authorization: Bearer <token> on the proxy call", async () => {
Expand Down
36 changes: 11 additions & 25 deletions tests/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,15 @@ describe("client.proxy.fetch — body translation", () => {

expect(fetchMock).toHaveBeenCalledTimes(1);
const url = fetchMock.mock.calls[0]?.[0] as string;
expect(url).toBe("https://proxy.test.com");
expect(url).toBe("https://proxy.test.com/ws-1/prj-1/env-1");
const opts = fetchMock.mock.calls[0]?.[1] as RequestInit;
expect(opts.method).toBe("POST");

const body = getCallBody(fetchMock.mock.calls[0] as unknown[]);
expect(body).toMatchObject({
url: "https://upstream/x?k=%K%",
method: "GET",
config: {
workspace: "ws-1",
project: "prj-1",
"environment-id": "env-1",
"is-personal": true,
},
"is-personal": true,
});
expect(body.body).toBeUndefined();
expect(body.headers).toBeUndefined();
Expand Down Expand Up @@ -211,7 +206,7 @@ describe("client.proxy.fetch — body translation", () => {
});

describe("client.proxy.request — low-level API", () => {
it("sends exact config in kebab-case", async () => {
it("sends wire body and routes context in URL path", async () => {
fetchMock.mockResolvedValue(new Response("{}", { status: 200 }));
const client = new Enkryptify(makeConfig());

Expand All @@ -226,13 +221,9 @@ describe("client.proxy.request — low-level API", () => {
url: "https://upstream/x",
method: "POST",
body: { foo: "%BAR%" },
config: {
workspace: "ws-1",
project: "prj-1",
"environment-id": "env-1",
"is-personal": true,
},
"is-personal": true,
});
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://proxy.test.com/ws-1/prj-1/env-1");
});

it("applies per-call environment override", async () => {
Expand All @@ -245,8 +236,7 @@ describe("client.proxy.request — low-level API", () => {
environment: "other-env",
});

const body = getCallBody(fetchMock.mock.calls[0] as unknown[]);
expect((body.config as Record<string, unknown>)["environment-id"]).toBe("other-env");
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://proxy.test.com/ws-1/prj-1/other-env");
});

it("applies per-call workspace/project/usePersonal overrides", async () => {
Expand All @@ -262,12 +252,8 @@ describe("client.proxy.request — low-level API", () => {
});

const body = getCallBody(fetchMock.mock.calls[0] as unknown[]);
expect(body.config).toEqual({
workspace: "other-ws",
project: "other-prj",
"environment-id": "env-1",
"is-personal": false,
});
expect(body["is-personal"]).toBe(false);
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://proxy.test.com/other-ws/other-prj/env-1");
});

it("rejects GET with body", async () => {
Expand Down Expand Up @@ -449,7 +435,7 @@ describe("client.proxy — URL resolution", () => {
const client = new Enkryptify(makeConfig({ proxy: { url: "https://config.test.com" } }));
await client.proxy.fetch("https://upstream/x");

expect(fetchMock.mock.calls[0]?.[0]).toBe("https://config.test.com");
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://config.test.com/ws-1/prj-1/env-1");
});

it("falls back to ENKRYPTIFY_PROXY_URL env var", async () => {
Expand All @@ -459,7 +445,7 @@ describe("client.proxy — URL resolution", () => {
const client = new Enkryptify(makeConfig({ proxy: undefined }));
await client.proxy.fetch("https://upstream/x");

expect(fetchMock.mock.calls[0]?.[0]).toBe("https://env.test.com");
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://env.test.com/ws-1/prj-1/env-1");
});

it("falls back to default POC URL when nothing else is set", async () => {
Expand All @@ -469,7 +455,7 @@ describe("client.proxy — URL resolution", () => {
const client = new Enkryptify(makeConfig({ proxy: undefined }));
await client.proxy.fetch("https://upstream/x");

expect(fetchMock.mock.calls[0]?.[0]).toBe("https://proxy.enkryptify.com");
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://proxy.enkryptify.com/ws-1/prj-1/env-1");
});
});

Expand Down
Loading