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
11,168 changes: 11,168 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions src/enkryptify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { KubernetesExchangeManager } from "@/kubernetes-exchange";
import { EnkryptifyProxy, sendProxyWire } from "@/proxy";
import { HttpInterceptor } from "@/interceptor";

const DEFAULT_PROXY_URL = "https://proxy.enkryptify.com";
const DEFAULT_PROXY_URL = "https://proxy.enkryptify.com/v1/proxy";

export class Enkryptify implements IEnkryptify {
#api: EnkryptifyApi;
Expand Down Expand Up @@ -121,7 +121,6 @@ export class Enkryptify implements IEnkryptify {
workspace: this.#workspace,
project: this.#project,
environment: this.#environment,
usePersonalValues: this.#usePersonalValues,
logger: this.#logger,
isDestroyed: () => this.#destroyed,
});
Expand All @@ -140,7 +139,6 @@ export class Enkryptify implements IEnkryptify {
workspace: this.#workspace,
project: this.#project,
environment: this.#environment,
usePersonalValues: this.#usePersonalValues,
},
logger: this.#logger,
});
Expand Down
10 changes: 3 additions & 7 deletions src/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export interface HttpInterceptorInit {
workspace: string;
project: string;
environment: string;
usePersonalValues: boolean;
};
logger: Logger;
}
Expand Down Expand Up @@ -215,12 +214,9 @@ 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,
};
}

Expand Down
34 changes: 15 additions & 19 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export interface EnkryptifyProxyInit {
workspace: string;
project: string;
environment: string;
usePersonalValues: boolean;
logger: Logger;
isDestroyed: () => boolean;
}
Expand All @@ -35,12 +34,9 @@ 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;
}

/**
Expand Down Expand Up @@ -82,15 +78,15 @@ export async function sendProxyWire(
const wireBody: Record<string, unknown> = {
url: body.url,
method: body.method,
config: body.config,
};
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 @@ -121,7 +117,6 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
#workspace: string;
#project: string;
#environment: string;
#usePersonalValues: boolean;

// Public-surface methods — rebound in the constructor so that
// `const { fetch } = client.proxy` (the pattern users need for wiring into
Expand All @@ -140,7 +135,6 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
this.#workspace = init.workspace;
this.#project = init.project;
this.#environment = init.environment;
this.#usePersonalValues = init.usePersonalValues;

this.fetch = this.#fetchImpl.bind(this);
this.request = this.#requestImpl.bind(this);
Expand Down Expand Up @@ -179,7 +173,7 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
method,
headers,
body,
config: this.#buildConfig(),
...this.#buildScope(),
},
init?.signal ?? null,
);
Expand All @@ -202,11 +196,10 @@ export class EnkryptifyProxy implements IEnkryptifyProxy {
);
}

const config = this.#buildConfig({
const scope = this.#buildScope({
workspace: options.workspace,
project: options.project,
environment: options.environment,
usePersonal: options.usePersonal,
});

return sendProxyWire(
Expand All @@ -216,23 +209,21 @@ 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"> {
return {
workspace: overrides?.workspace ?? this.#workspace,
project: overrides?.project ?? this.#project,
"environment-id": overrides?.environment ?? this.#environment,
"is-personal": overrides?.usePersonal ?? this.#usePersonalValues,
};
}
}
Expand Down Expand Up @@ -309,3 +300,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)}`;
Comment on lines +304 to +306

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve /v1/proxy for user-supplied proxy bases.

DEFAULT_PROXY_URL was updated to include /v1/proxy, but this helper still appends scope directly onto whatever came from config.proxy.url / ENKRYPTIFY_PROXY_URL. A host-only override like "https://proxy.example.com" now becomes https://proxy.example.com/ws/prj/env, which misses the new backend route and breaks existing custom proxy configs.

Possible fix
 function buildProxyRequestUrl(baseUrl: string, workspace: string, project: string, environmentId: string): string {
     const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
-    return `${normalizedBaseUrl}/${encodeURIComponent(workspace)}/${encodeURIComponent(project)}/${encodeURIComponent(environmentId)}`;
+    const routedBaseUrl = normalizedBaseUrl.endsWith("/v1/proxy")
+        ? normalizedBaseUrl
+        : `${normalizedBaseUrl}/v1/proxy`;
+    return `${routedBaseUrl}/${encodeURIComponent(workspace)}/${encodeURIComponent(project)}/${encodeURIComponent(environmentId)}`;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function buildProxyRequestUrl(baseUrl: string, workspace: string, project: string, environmentId: string): string {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
return `${normalizedBaseUrl}/${encodeURIComponent(workspace)}/${encodeURIComponent(project)}/${encodeURIComponent(environmentId)}`;
function buildProxyRequestUrl(baseUrl: string, workspace: string, project: string, environmentId: string): string {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
const routedBaseUrl = normalizedBaseUrl.endsWith("/v1/proxy")
? normalizedBaseUrl
: `${normalizedBaseUrl}/v1/proxy`;
return `${routedBaseUrl}/${encodeURIComponent(workspace)}/${encodeURIComponent(project)}/${encodeURIComponent(environmentId)}`;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/proxy.ts` around lines 304 - 306, The helper buildProxyRequestUrl
currently appends workspace/project/environment directly to the normalized base
and can drop the required /v1/proxy segment; update buildProxyRequestUrl to
detect if normalizedBaseUrl already contains the "/v1/proxy" path (allow
optional trailing slash) and, if it does not, insert "/v1/proxy" before
appending the encoded workspace/project/environment; keep using
encodeURIComponent for workspace/project/environment and ensure you trim
duplicate slashes when concatenating.

}
4 changes: 0 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,6 @@ export interface ProxyRequestOptions {
project?: string;
/** Override the client's environment for this request. */
environment?: string;
/** Override the client's `usePersonalValues` setting for this request. */
usePersonal?: boolean;
}

export interface IEnkryptifyProxy {
Expand Down Expand Up @@ -250,8 +248,6 @@ export interface InterceptorRule {
project?: string;
/** Override the client's environment for this rule. */
environment?: string;
/** Override the client's `usePersonalValues` setting for this rule. */
usePersonal?: boolean;

/**
* How to handle intercepted requests whose body cannot be represented on
Expand Down
27 changes: 9 additions & 18 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 @@ -377,16 +377,12 @@ 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,
});
await findProxyCall(fetchMock);
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 () => {
it("rule-level workspace/project/environment override defaults", async () => {
fetchMock.mockResolvedValue(new Response("{}", { status: 200 }));

activeClient = new Enkryptify(
Expand All @@ -399,7 +395,6 @@ describe("interceptor — ProxyWireBody shape", () => {
workspace: "override-ws",
project: "override-prj",
environment: "override-env",
usePersonal: false,
},
],
},
Expand All @@ -409,13 +404,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,
});
await findProxyCall(fetchMock);
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
37 changes: 9 additions & 28 deletions tests/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,14 @@

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/v1/proxy/ws-1/prj-1/env-1");

Check failure on line 50 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (25)

tests/proxy.test.ts > client.proxy.fetch — body translation > GET without body sends correct wire body

AssertionError: expected 'https://proxy.test.com/ws-1/prj-1/env…' to be 'https://proxy.test.com/v1/proxy/ws-1/…' // Object.is equality Expected: "https://proxy.test.com/v1/proxy/ws-1/prj-1/env-1" Received: "https://proxy.test.com/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:50:21

Check failure on line 50 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (22)

tests/proxy.test.ts > client.proxy.fetch — body translation > GET without body sends correct wire body

AssertionError: expected 'https://proxy.test.com/ws-1/prj-1/env…' to be 'https://proxy.test.com/v1/proxy/ws-1/…' // Object.is equality Expected: "https://proxy.test.com/v1/proxy/ws-1/prj-1/env-1" Received: "https://proxy.test.com/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:50:21

Check failure on line 50 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (24)

tests/proxy.test.ts > client.proxy.fetch — body translation > GET without body sends correct wire body

AssertionError: expected 'https://proxy.test.com/ws-1/prj-1/env…' to be 'https://proxy.test.com/v1/proxy/ws-1/…' // Object.is equality Expected: "https://proxy.test.com/v1/proxy/ws-1/prj-1/env-1" Received: "https://proxy.test.com/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:50:21

Check failure on line 50 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (20)

tests/proxy.test.ts > client.proxy.fetch — body translation > GET without body sends correct wire body

AssertionError: expected 'https://proxy.test.com/ws-1/prj-1/env…' to be 'https://proxy.test.com/v1/proxy/ws-1/…' // Object.is equality Expected: "https://proxy.test.com/v1/proxy/ws-1/prj-1/env-1" Received: "https://proxy.test.com/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:50:21
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,
},
});
expect(body.body).toBeUndefined();
expect(body.headers).toBeUndefined();
Expand Down Expand Up @@ -211,7 +205,7 @@
});

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 +220,8 @@
url: "https://upstream/x",
method: "POST",
body: { foo: "%BAR%" },
config: {
workspace: "ws-1",
project: "prj-1",
"environment-id": "env-1",
"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,11 +234,10 @@
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 () => {
it("applies per-call workspace/project overrides", async () => {
fetchMock.mockResolvedValue(new Response("{}", { status: 200 }));
const client = new Enkryptify(makeConfig());

Expand All @@ -258,16 +246,9 @@
method: "GET",
workspace: "other-ws",
project: "other-prj",
usePersonal: false,
});

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(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 +430,7 @@
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 +440,7 @@
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 +450,7 @@
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");

Check failure on line 453 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (25)

tests/proxy.test.ts > client.proxy — URL resolution > falls back to default POC URL when nothing else is set

AssertionError: expected 'https://proxy.enkryptify.com/v1/proxy…' to be 'https://proxy.enkryptify.com/ws-1/prj…' // Object.is equality Expected: "https://proxy.enkryptify.com/ws-1/prj-1/env-1" Received: "https://proxy.enkryptify.com/v1/proxy/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:453:46

Check failure on line 453 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (22)

tests/proxy.test.ts > client.proxy — URL resolution > falls back to default POC URL when nothing else is set

AssertionError: expected 'https://proxy.enkryptify.com/v1/proxy…' to be 'https://proxy.enkryptify.com/ws-1/prj…' // Object.is equality Expected: "https://proxy.enkryptify.com/ws-1/prj-1/env-1" Received: "https://proxy.enkryptify.com/v1/proxy/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:453:46

Check failure on line 453 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (24)

tests/proxy.test.ts > client.proxy — URL resolution > falls back to default POC URL when nothing else is set

AssertionError: expected 'https://proxy.enkryptify.com/v1/proxy…' to be 'https://proxy.enkryptify.com/ws-1/prj…' // Object.is equality Expected: "https://proxy.enkryptify.com/ws-1/prj-1/env-1" Received: "https://proxy.enkryptify.com/v1/proxy/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:453:46

Check failure on line 453 in tests/proxy.test.ts

View workflow job for this annotation

GitHub Actions / ci (20)

tests/proxy.test.ts > client.proxy — URL resolution > falls back to default POC URL when nothing else is set

AssertionError: expected 'https://proxy.enkryptify.com/v1/proxy…' to be 'https://proxy.enkryptify.com/ws-1/prj…' // Object.is equality Expected: "https://proxy.enkryptify.com/ws-1/prj-1/env-1" Received: "https://proxy.enkryptify.com/v1/proxy/ws-1/prj-1/env-1" ❯ tests/proxy.test.ts:453:46
});
});

Expand Down
Loading