Skip to content

Commit 9b48363

Browse files
committed
Standardize source credential bindings
1 parent 1d413ce commit 9b48363

56 files changed

Lines changed: 1011 additions & 1403 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cloud/src/services/sources-api.node.test.ts

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -760,13 +760,14 @@ describe("sources api (HTTP)", () => {
760760
value: "alice-secret",
761761
},
762762
});
763-
const binding = yield* client.openapi.setSourceBinding({
763+
const binding = yield* client.credentialBindings.set({
764764
params: { scopeId: ScopeId.make(aliceScope) },
765765
payload: {
766+
targetScope: ScopeId.make(aliceScope),
767+
pluginId: "openapi",
766768
sourceId: namespace,
767769
sourceScope: ScopeId.make(orgId),
768-
scope: ScopeId.make(aliceScope),
769-
slot: "auth:personal-token",
770+
slotKey: "auth:personal-token",
770771
value: {
771772
kind: "secret",
772773
secretId: SecretId.make("alice_pat"),
@@ -777,7 +778,7 @@ describe("sources api (HTTP)", () => {
777778
sourceId: namespace,
778779
sourceScopeId: ScopeId.make(orgId),
779780
scopeId: ScopeId.make(aliceScope),
780-
slot: "auth:personal-token",
781+
slotKey: "auth:personal-token",
781782
value: {
782783
kind: "secret",
783784
secretId: SecretId.make("alice_pat"),
@@ -798,13 +799,14 @@ describe("sources api (HTTP)", () => {
798799
value: "bob-secret",
799800
},
800801
});
801-
yield* client.openapi.setSourceBinding({
802+
yield* client.credentialBindings.set({
802803
params: { scopeId: ScopeId.make(bobScope) },
803804
payload: {
805+
targetScope: ScopeId.make(bobScope),
806+
pluginId: "openapi",
804807
sourceId: namespace,
805808
sourceScope: ScopeId.make(orgId),
806-
scope: ScopeId.make(bobScope),
807-
slot: "auth:personal-token",
809+
slotKey: "auth:personal-token",
808810
value: {
809811
kind: "secret",
810812
secretId: SecretId.make("bob_pat"),
@@ -815,18 +817,19 @@ describe("sources api (HTTP)", () => {
815817
);
816818

817819
const aliceBindings = yield* asUser(aliceId, orgId, (client) =>
818-
client.openapi.listSourceBindings({
820+
client.credentialBindings.listForSource({
819821
params: {
820822
scopeId: ScopeId.make(aliceScope),
821-
namespace,
822-
sourceScopeId: ScopeId.make(orgId),
823+
pluginId: "openapi",
824+
sourceId: namespace,
825+
sourceScope: ScopeId.make(orgId),
823826
},
824827
}),
825828
);
826829
expect(aliceBindings).toContainEqual(
827830
expect.objectContaining({
828831
scopeId: ScopeId.make(aliceScope),
829-
slot: "auth:personal-token",
832+
slotKey: "auth:personal-token",
830833
value: {
831834
kind: "secret",
832835
secretId: SecretId.make("alice_pat"),
@@ -837,25 +840,26 @@ describe("sources api (HTTP)", () => {
837840
expect(
838841
aliceBindings.some(
839842
(binding) =>
840-
binding.slot === "auth:personal-token" &&
843+
binding.slotKey === "auth:personal-token" &&
841844
binding.value.kind === "secret" &&
842845
binding.value.secretId === SecretId.make("bob_pat"),
843846
),
844847
).toBe(false);
845848

846849
const bobBindings = yield* asUser(bobId, orgId, (client) =>
847-
client.openapi.listSourceBindings({
850+
client.credentialBindings.listForSource({
848851
params: {
849852
scopeId: ScopeId.make(bobScope),
850-
namespace,
851-
sourceScopeId: ScopeId.make(orgId),
853+
pluginId: "openapi",
854+
sourceId: namespace,
855+
sourceScope: ScopeId.make(orgId),
852856
},
853857
}),
854858
);
855859
expect(bobBindings).toContainEqual(
856860
expect.objectContaining({
857861
scopeId: ScopeId.make(bobScope),
858-
slot: "auth:personal-token",
862+
slotKey: "auth:personal-token",
859863
value: {
860864
kind: "secret",
861865
secretId: SecretId.make("bob_pat"),
@@ -866,7 +870,7 @@ describe("sources api (HTTP)", () => {
866870
expect(
867871
bobBindings.some(
868872
(binding) =>
869-
binding.slot === "auth:personal-token" &&
873+
binding.slotKey === "auth:personal-token" &&
870874
binding.value.kind === "secret" &&
871875
binding.value.secretId === SecretId.make("alice_pat"),
872876
),

apps/cloud/src/services/tenant-isolation.node.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,14 @@ describe("tenant isolation (HTTP)", () => {
272272
},
273273
},
274274
});
275-
yield* client.openapi.setSourceBinding({
275+
yield* client.credentialBindings.set({
276276
params: { scopeId: ScopeId.make(orgA) },
277277
payload: {
278+
targetScope: ScopeId.make(orgA),
279+
pluginId: "openapi",
278280
sourceId: namespaceA,
279281
sourceScope: ScopeId.make(orgA),
280-
scope: ScopeId.make(orgA),
281-
slot: "auth:token",
282+
slotKey: "auth:token",
282283
value: { kind: "secret", secretId: secretIdA },
283284
},
284285
});
@@ -319,13 +320,14 @@ describe("tenant isolation (HTTP)", () => {
319320
},
320321
},
321322
});
322-
yield* client.openapi.setSourceBinding({
323+
yield* client.credentialBindings.set({
323324
params: { scopeId: ScopeId.make(orgA) },
324325
payload: {
326+
targetScope: ScopeId.make(orgA),
327+
pluginId: "openapi",
325328
sourceId: namespaceA,
326329
sourceScope: ScopeId.make(orgA),
327-
scope: ScopeId.make(orgA),
328-
slot: "auth:conn",
330+
slotKey: "auth:conn",
329331
value: { kind: "connection", connectionId: connectionIdA },
330332
},
331333
});

packages/core/api/src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ExecutionsApi } from "./executions/api";
99
import { ScopeApi } from "./scope/api";
1010
import { OAuthApi } from "./oauth/api";
1111
import { PoliciesApi } from "./policies/api";
12+
import { CredentialBindingsApi } from "./credential-bindings/api";
1213

1314
export const CoreExecutorApi = HttpApi.make("executor")
1415
.add(ToolsApi)
@@ -19,6 +20,7 @@ export const CoreExecutorApi = HttpApi.make("executor")
1920
.add(ScopeApi)
2021
.add(OAuthApi)
2122
.add(PoliciesApi)
23+
.add(CredentialBindingsApi)
2224
.annotateMerge(
2325
OpenApi.annotations({
2426
title: "Executor API",
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { HttpApiBuilder } from "effect/unstable/httpapi";
2+
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
3+
import {
4+
HttpClient,
5+
HttpClientError,
6+
HttpClientRequest,
7+
HttpClientResponse,
8+
HttpRouter,
9+
HttpServer,
10+
} from "effect/unstable/http";
11+
import { describe, expect, it } from "@effect/vitest";
12+
import { Context, Effect, Layer } from "effect";
13+
14+
import {
15+
Scope,
16+
ScopeId,
17+
createExecutor,
18+
definePlugin,
19+
makeTestConfig,
20+
type Executor,
21+
} from "@executor-js/sdk";
22+
23+
import { ExecutorApi } from "./api";
24+
import { observabilityMiddleware } from "./observability";
25+
import { CoreHandlers, ExecutionEngineService, ExecutorService } from "./server";
26+
27+
const TEST_PLUGIN_ID = "credentialApiTest";
28+
const TEST_SOURCE_ID = "shared-api";
29+
const TEST_SLOT = "request.header.Authorization";
30+
31+
const webHandlerFor = (executor: Executor) =>
32+
Effect.acquireRelease(
33+
Effect.sync(() =>
34+
HttpRouter.toWebHandler(
35+
HttpApiBuilder.layer(ExecutorApi).pipe(
36+
Layer.provide(CoreHandlers),
37+
Layer.provide(observabilityMiddleware(ExecutorApi)),
38+
Layer.provide(Layer.succeed(ExecutorService)(executor)),
39+
Layer.provide(
40+
Layer.succeed(ExecutionEngineService)({} as ExecutionEngineService["Service"]),
41+
),
42+
Layer.provideMerge(HttpServer.layerServices),
43+
Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),
44+
),
45+
{ disableLogger: true },
46+
),
47+
),
48+
(web) => Effect.promise(() => web.dispose()),
49+
);
50+
51+
const handlerContextFor = (executor: Executor) =>
52+
Context.make(ExecutorService, executor).pipe(
53+
Context.add(ExecutionEngineService, {} as ExecutionEngineService["Service"]),
54+
);
55+
56+
const apiClientFor = (executor: Executor) =>
57+
Effect.gen(function* () {
58+
const web = yield* webHandlerFor(executor);
59+
const context = handlerContextFor(executor);
60+
const httpClient = HttpClient.make((request, _url, signal) =>
61+
Effect.gen(function* () {
62+
const webRequest = yield* HttpClientRequest.toWeb(request, { signal }).pipe(
63+
Effect.mapError(
64+
(cause) =>
65+
new HttpClientError.HttpClientError({
66+
reason: new HttpClientError.InvalidUrlError({ request, cause }),
67+
}),
68+
),
69+
);
70+
const response = yield* Effect.promise(() => web.handler(webRequest, context));
71+
return HttpClientResponse.fromWeb(request, response);
72+
}),
73+
);
74+
return yield* HttpApiClient.makeWith(ExecutorApi, {
75+
httpClient,
76+
baseUrl: "http://localhost",
77+
});
78+
});
79+
80+
const scope = (id: ScopeId, name: string) => Scope.make({ id, name, createdAt: new Date() });
81+
82+
const credentialApiTestPlugin = definePlugin(() => ({
83+
id: TEST_PLUGIN_ID,
84+
storage: () => ({}),
85+
extension: (ctx) => ({
86+
registerSource: (targetScope: ScopeId) =>
87+
ctx.core.sources.register({
88+
id: TEST_SOURCE_ID,
89+
scope: targetScope,
90+
kind: "test-api",
91+
name: "Shared API",
92+
tools: [{ name: "read", description: "read from the shared API" }],
93+
}),
94+
}),
95+
}));
96+
97+
describe("credential binding API", () => {
98+
it.effect("sets, lists, and removes source credential bindings", () =>
99+
Effect.gen(function* () {
100+
const userScope = ScopeId.make("api-user");
101+
const orgScope = ScopeId.make("api-org");
102+
const executor = yield* createExecutor(
103+
makeTestConfig({
104+
scopes: [scope(userScope, "user"), scope(orgScope, "org")],
105+
plugins: [credentialApiTestPlugin()] as const,
106+
}),
107+
);
108+
yield* executor.credentialApiTest.registerSource(orgScope);
109+
const client = yield* apiClientFor(executor);
110+
111+
const created = yield* client.credentialBindings.set({
112+
params: { scopeId: userScope },
113+
payload: {
114+
targetScope: userScope,
115+
pluginId: TEST_PLUGIN_ID,
116+
sourceId: TEST_SOURCE_ID,
117+
sourceScope: orgScope,
118+
slotKey: TEST_SLOT,
119+
value: { kind: "text", text: "test-token" },
120+
},
121+
});
122+
expect(created).toMatchObject({
123+
slotKey: TEST_SLOT,
124+
scopeId: String(userScope),
125+
value: { kind: "text", text: "test-token" },
126+
});
127+
128+
const listed = yield* client.credentialBindings.listForSource({
129+
params: {
130+
scopeId: userScope,
131+
pluginId: TEST_PLUGIN_ID,
132+
sourceId: TEST_SOURCE_ID,
133+
sourceScope: orgScope,
134+
},
135+
});
136+
expect(listed).toHaveLength(1);
137+
expect(listed[0]).toMatchObject({ slotKey: TEST_SLOT, scopeId: String(userScope) });
138+
139+
const removed = yield* client.credentialBindings.remove({
140+
params: { scopeId: userScope },
141+
payload: {
142+
targetScope: userScope,
143+
pluginId: TEST_PLUGIN_ID,
144+
sourceId: TEST_SOURCE_ID,
145+
sourceScope: orgScope,
146+
slotKey: TEST_SLOT,
147+
},
148+
});
149+
expect(removed).toEqual({ removed: true });
150+
151+
const afterRemove = yield* client.credentialBindings.listForSource({
152+
params: {
153+
scopeId: userScope,
154+
pluginId: TEST_PLUGIN_ID,
155+
sourceId: TEST_SOURCE_ID,
156+
sourceScope: orgScope,
157+
},
158+
});
159+
expect(afterRemove).toEqual([]);
160+
}),
161+
);
162+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
2+
import { Schema } from "effect";
3+
import {
4+
CredentialBindingRef,
5+
RemoveCredentialBindingInput,
6+
ScopeId,
7+
SetCredentialBindingInput,
8+
} from "@executor-js/sdk";
9+
10+
import { InternalError } from "../observability";
11+
12+
const ScopeParams = { scopeId: ScopeId };
13+
const CredentialBindingSourceParams = {
14+
scopeId: ScopeId,
15+
pluginId: Schema.String,
16+
sourceId: Schema.String,
17+
sourceScope: ScopeId,
18+
};
19+
20+
export const CredentialBindingsApi = HttpApiGroup.make("credentialBindings")
21+
.add(
22+
HttpApiEndpoint.get(
23+
"listForSource",
24+
"/scopes/:scopeId/credential-bindings/:pluginId/sources/:sourceId/base/:sourceScope",
25+
{
26+
params: CredentialBindingSourceParams,
27+
success: Schema.Array(CredentialBindingRef),
28+
error: InternalError,
29+
},
30+
),
31+
)
32+
.add(
33+
HttpApiEndpoint.post("set", "/scopes/:scopeId/credential-bindings", {
34+
params: ScopeParams,
35+
payload: SetCredentialBindingInput,
36+
success: CredentialBindingRef,
37+
error: InternalError,
38+
}),
39+
)
40+
.add(
41+
HttpApiEndpoint.post("remove", "/scopes/:scopeId/credential-bindings/remove", {
42+
params: ScopeParams,
43+
payload: RemoveCredentialBindingInput,
44+
success: Schema.Struct({ removed: Schema.Boolean }),
45+
error: InternalError,
46+
}),
47+
);

0 commit comments

Comments
 (0)