Route hosted plugin outbound requests through a shared Effect HttpClient layer that blocks local/private/metadata targets and re-checks redirects. Thread the executor-provided HTTP client through OpenAPI, GraphQL, MCP probing, and OAuth discovery, keep local harnesses on the normal fetch client, and split the restored security coverage into the relevant existing suites. Fix cloud migration scripts to invoke the local Drizzle binary under op run.
Showing the changes to apps/cloud/src (7 of 19 files). Show all
Unified · Split
| @@ -33,6 +33,7 @@ import { · 32 unmodified lines | |||
| 33 | createExecutor, | 33 | createExecutor, |
| 34 | definePlugin, | 34 | definePlugin, |
| 35 | } from "@executor-js/sdk"; | 35 | } from "@executor-js/sdk"; |
| 36 | import { FetchHttpClient } from "effect/unstable/http"; | ||
| 36 | import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres"; | 37 | import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres"; |
| 37 | import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing"; | 38 | import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing"; |
| 38 | import executorConfig from "../executor.config"; | 39 | import executorConfig from "../executor.config"; |
| @@ -117,6 +118,7 @@ const buildScopedExecutor = (scopeId: string, scopeName: string, options: BuildO · 78 unmodified lines | |||
| 117 | adapter, | 118 | adapter, |
| 118 | blobs, | 119 | blobs, |
| 119 | plugins, | 120 | plugins, |
| 121 | httpClientLayer: FetchHttpClient.layer, | ||
| 120 | onElicitation: "accept-all", | 122 | onElicitation: "accept-all", |
| 121 | }); | 123 | }); |
| 122 | }); | 124 | }); |
| @@ -26,7 +26,12 @@ import { · 25 unmodified lines | |||
| 26 | } from "@executor-js/api/server"; | 26 | } from "@executor-js/api/server"; |
| 27 | import { createExecutionEngine } from "@executor-js/execution"; | 27 | import { createExecutionEngine } from "@executor-js/execution"; |
| 28 | import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; | 28 | import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; |
| 29 | import { Scope, ScopeId, collectSchemas, createExecutor } from "@executor-js/sdk"; | 29 | import { |
| 30 | Scope, | ||
| 31 | ScopeId, | ||
| 32 | collectSchemas, | ||
| 33 | createExecutor, | ||
| 34 | } from "@executor-js/sdk"; | ||
| 30 | import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres"; | 35 | import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres"; |
| 31 | import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing"; | 36 | import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing"; |
| 32 | 37 | ||
| @@ -61,7 +66,10 @@ const defaultUserFor = (orgId: string) => `default_user_${orgId}`; · 28 unmodified lines | |||
| 61 | // --------------------------------------------------------------------------- | 66 | // --------------------------------------------------------------------------- |
| 62 | 67 | ||
| 63 | const fakeVault = makeTestWorkOSVaultClient(); | 68 | const fakeVault = makeTestWorkOSVaultClient(); |
| 64 | const testPlugins = executorConfig.plugins({ workosVaultClient: fakeVault }); | 69 | const testPlugins = executorConfig.plugins({ |
| 70 | workosVaultClient: fakeVault, | ||
| 71 | }); | ||
| 72 | const testHttpClientLayer = FetchHttpClient.layer; | ||
| 65 | 73 | ||
| 66 | const createTestScopedExecutor = (userId: string, orgId: string, orgName: string) => | 74 | const createTestScopedExecutor = (userId: string, orgId: string, orgName: string) => |
| 67 | Effect.gen(function* () { | 75 | Effect.gen(function* () { |
| @@ -85,6 +93,7 @@ const createTestScopedExecutor = (userId: string, orgId: string, orgName: string · 17 unmodified lines | |||
| 85 | adapter, | 93 | adapter, |
| 86 | blobs, | 94 | blobs, |
| 87 | plugins, | 95 | plugins, |
| 96 | httpClientLayer: testHttpClientLayer, | ||
| 88 | onElicitation: "accept-all", | 97 | onElicitation: "accept-all", |
| 89 | }); | 98 | }); |
| 90 | }); | 99 | }); |
| @@ -10,7 +10,13 @@ · 9 unmodified lines | |||
| 10 | 10 | ||
| 11 | import { Effect } from "effect"; | 11 | import { Effect } from "effect"; |
| 12 | 12 | ||
| 13 | import { Scope, ScopeId, collectSchemas, createExecutor } from "@executor-js/sdk"; | 13 | import { |
| 14 | Scope, | ||
| 15 | ScopeId, | ||
| 16 | collectSchemas, | ||
| 17 | createExecutor, | ||
| 18 | makeHostedHttpClientLayer, | ||
| 19 | } from "@executor-js/sdk"; | ||
| 14 | import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres"; | 20 | import { makePostgresAdapter, makePostgresBlobStore } from "@executor-js/storage-postgres"; |
| 15 | 21 | ||
| 16 | import { env } from "cloudflare:workers"; | 22 | import { env } from "cloudflare:workers"; |
| @@ -58,6 +64,7 @@ export const createScopedExecutor = ( · 41 unmodified lines | |||
| 58 | const { db } = yield* DbService; | 64 | const { db } = yield* DbService; |
| 59 | 65 | ||
| 60 | const plugins = orgPlugins(); | 66 | const plugins = orgPlugins(); |
| 67 | const httpClientLayer = makeHostedHttpClientLayer(); | ||
| 61 | const schema = collectSchemas(plugins); | 68 | const schema = collectSchemas(plugins); |
| 62 | const adapter = makePostgresAdapter({ db, schema }); | 69 | const adapter = makePostgresAdapter({ db, schema }); |
| 63 | const blobs = makePostgresBlobStore({ db }); | 70 | const blobs = makePostgresBlobStore({ db }); |
| @@ -82,6 +89,7 @@ export const createScopedExecutor = ( · 18 unmodified lines | |||
| 82 | adapter, | 89 | adapter, |
| 83 | blobs, | 90 | blobs, |
| 84 | plugins, | 91 | plugins, |
| 92 | httpClientLayer, | ||
| 85 | onElicitation: "accept-all", | 93 | onElicitation: "accept-all", |
| 86 | }); | 94 | }); |
| 87 | }); | 95 | }); |
| @@ -27,10 +27,10 @@ import { createServer, type Server } from "node:http"; · 26 unmodified lines | |||
| 27 | import type { AddressInfo } from "node:net"; | 27 | import type { AddressInfo } from "node:net"; |
| 28 | import { createHash, randomBytes } from "node:crypto"; | 28 | import { createHash, randomBytes } from "node:crypto"; |
| 29 | 29 | ||
| 30 | import { Effect, Option, Schema } from "effect"; | 30 | import { Effect, Option, Result, Schema } from "effect"; |
| 31 | import { ScopeId } from "@executor-js/sdk"; | 31 | import { ScopeId } from "@executor-js/sdk"; |
| 32 | 32 | ||
| 33 | import { asUser, testUserOrgScopeId } from "./__test-harness__/api-harness"; | 33 | import { asOrg, asUser, testUserOrgScopeId } from "./__test-harness__/api-harness"; |
| 34 | 34 | ||
| 35 | // --------------------------------------------------------------------------- | 35 | // --------------------------------------------------------------------------- |
| 36 | // Fake OAuth + MCP server | 36 | // Fake OAuth + MCP server |
| @@ -262,6 +262,67 @@ afterAll(async () => { · 225 unmodified lines | |||
| 262 | // --------------------------------------------------------------------------- | 262 | // --------------------------------------------------------------------------- |
| 263 | 263 | ||
| 264 | describe("mcp oauth end-to-end (node pool, real OAuth + MCP server)", () => { | 264 | describe("mcp oauth end-to-end (node pool, real OAuth + MCP server)", () => { |
| 265 | it.effect( | ||
| 266 | "start rejects a redirectUrl on a different origin before discovery", | ||
| 267 | () => | ||
| 268 | Effect.gen(function* () { | ||
| 269 | const org = `org_${crypto.randomUUID()}`; | ||
| 270 | const scopeId = ScopeId.make(org); | ||
| 271 | const start = Date.now(); | ||
| 272 | |||
| 273 | const result = yield* asOrg(org, (client) => | ||
| 274 | client.oauth.start({ | ||
| 275 | params: { scopeId }, | ||
| 276 | payload: { | ||
| 277 | endpoint: "https://example.test/api", | ||
| 278 | redirectUrl: "https://other.example/cb", | ||
| 279 | connectionId: "conn-foreign-redirect", | ||
| 280 | tokenScope: String(scopeId), | ||
| 281 | pluginId: "mcp", | ||
| 282 | strategy: { kind: "dynamic-dcr" }, | ||
| 283 | }, | ||
| 284 | }), | ||
| 285 | ).pipe(Effect.result); | ||
| 286 | |||
| 287 | expect(Result.isFailure(result)).toBe(true); | ||
| 288 | expect(Date.now() - start).toBeLessThan(1000); | ||
| 289 | }), | ||
| 290 | { timeout: 30_000 }, | ||
| 291 | ); | ||
| 292 | |||
| 293 | it.effect( | ||
| 294 | "start rejects non-http redirectUrl schemes", | ||
| 295 | () => | ||
| 296 | Effect.gen(function* () { | ||
| 297 | const org = `org_${crypto.randomUUID()}`; | ||
| 298 | const scopeId = ScopeId.make(org); | ||
| 299 | |||
| 300 | for (const redirectUrl of [ | ||
| 301 | "javascript:alert(1)", | ||
| 302 | "data:text/html,<script>alert(1)</script>", | ||
| 303 | ]) { | ||
| 304 | const start = Date.now(); | ||
| 305 | const result = yield* asOrg(org, (client) => | ||
| 306 | client.oauth.start({ | ||
| 307 | params: { scopeId }, | ||
| 308 | payload: { | ||
| 309 | endpoint: "https://example.test/api", | ||
| 310 | redirectUrl, | ||
| 311 | connectionId: `conn-${crypto.randomUUID().slice(0, 8)}`, | ||
| 312 | tokenScope: String(scopeId), | ||
| 313 | pluginId: "mcp", | ||
| 314 | strategy: { kind: "dynamic-dcr" }, | ||
| 315 | }, | ||
| 316 | }), | ||
| 317 | ).pipe(Effect.result); | ||
| 318 | |||
| 319 | expect(Result.isFailure(result)).toBe(true); | ||
| 320 | expect(Date.now() - start).toBeLessThan(1000); | ||
| 321 | } | ||
| 322 | }), | ||
| 323 | { timeout: 30_000 }, | ||
| 324 | ); | ||
| 325 | |||
| 265 | it.effect( | 326 | it.effect( |
| 266 | "startOAuth → authorize → completeOAuth writes tokens at the invoker scope", | 327 | "startOAuth → authorize → completeOAuth writes tokens at the invoker scope", |
| 267 | () => | 328 | () => |
| @@ -9,24 +9,27 @@ import { ScopeId, SecretId } from "@executor-js/sdk"; · 8 unmodified lines | |||
| 9 | import { asOrg, fetchForOrg, TEST_BASE_URL } from "./__test-harness__/api-harness"; | 9 | import { asOrg, fetchForOrg, TEST_BASE_URL } from "./__test-harness__/api-harness"; |
| 10 | 10 | ||
| 11 | describe("secrets api (HTTP)", () => { | 11 | describe("secrets api (HTTP)", () => { |
| 12 | it.effect("set → list → status round-trips a new secret without exposing plaintext", () => | 12 | it.effect("set → list → status returns secret metadata", () => |
| 13 | Effect.gen(function* () { | 13 | Effect.gen(function* () { |
| 14 | const org = `org_${crypto.randomUUID()}`; | 14 | const org = `org_${crypto.randomUUID()}`; |
| 15 | const id = `sec_${crypto.randomUUID().slice(0, 8)}`; | 15 | const id = `sec_${crypto.randomUUID().slice(0, 8)}`; |
| 16 | 16 | ||
| 17 | const secretValue = "sk-test-abc"; | ||
| 17 | const setRef = yield* asOrg(org, (client) => | 18 | const setRef = yield* asOrg(org, (client) => |
| 18 | client.secrets.set({ | 19 | client.secrets.set({ |
| 19 | params: { scopeId: ScopeId.make(org) }, | 20 | params: { scopeId: ScopeId.make(org) }, |
| 20 | payload: { id: SecretId.make(id), name: "My API Token", value: "sk-test-abc" }, | 21 | payload: { id: SecretId.make(id), name: "My API Token", value: secretValue }, |
| 21 | }), | 22 | }), |
| 22 | ); | 23 | ); |
| 23 | expect(setRef.id).toBe(id); | 24 | expect(setRef.id).toBe(id); |
| 24 | expect(setRef.scopeId).toBe(org); | 25 | expect(setRef.scopeId).toBe(org); |
| 26 | expect(JSON.stringify(setRef)).not.toContain(secretValue); | ||
| 25 | 27 | ||
| 26 | const list = yield* asOrg(org, (client) => | 28 | const list = yield* asOrg(org, (client) => |
| 27 | client.secrets.list({ params: { scopeId: ScopeId.make(org) } }), | 29 | client.secrets.list({ params: { scopeId: ScopeId.make(org) } }), |
| 28 | ); | 30 | ); |
| 29 | expect(list.find((s) => s.id === id)?.name).toBe("My API Token"); | 31 | expect(list.find((s) => s.id === id)?.name).toBe("My API Token"); |
| 32 | expect(JSON.stringify(list)).not.toContain(secretValue); | ||
| 30 | 33 | ||
| 31 | const status = yield* asOrg(org, (client) => | 34 | const status = yield* asOrg(org, (client) => |
| 32 | client.secrets.status({ | 35 | client.secrets.status({ |
| @@ -37,7 +40,7 @@ describe("secrets api (HTTP)", () => { · 4 unmodified lines | |||
| 37 | }), | 40 | }), |
| 38 | ); | 41 | ); |
| 39 | 42 | ||
| 40 | it.effect("does not expose a plaintext resolve endpoint", () => | 43 | it.effect("resolve is not available through the public API", () => |
| 41 | Effect.gen(function* () { | 44 | Effect.gen(function* () { |
| 42 | const org = `org_${crypto.randomUUID()}`; | 45 | const org = `org_${crypto.randomUUID()}`; |
| 43 | const id = `sec_${crypto.randomUUID().slice(0, 8)}`; | 46 | const id = `sec_${crypto.randomUUID().slice(0, 8)}`; |
| @@ -373,6 +373,26 @@ describe("sources api (HTTP)", () => { · 372 unmodified lines | |||
| 373 | }), | 373 | }), |
| 374 | ); | 374 | ); |
| 375 | 375 | ||
| 376 | it.effect("openapi.addSpec accepts a public HTTP baseUrl in local mode", () => | ||
| 377 | Effect.gen(function* () { | ||
| 378 | const org = `org_${crypto.randomUUID()}`; | ||
| 379 | |||
| 380 | const result = yield* asOrg(org, (client) => | ||
| 381 | client.openapi.addSpec({ | ||
| 382 | params: { scopeId: ScopeId.make(org) }, | ||
| 383 | payload: { | ||
| 384 | targetScope: ScopeId.make(org), | ||
| 385 | spec: MINIMAL_OPENAPI_SPEC, | ||
| 386 | namespace: `ns_${crypto.randomUUID().replace(/-/g, "_")}`, | ||
| 387 | baseUrl: "http://example.com", | ||
| 388 | }, | ||
| 389 | }), | ||
| 390 | ); | ||
| 391 | |||
| 392 | expect(result.toolCount).toBe(1); | ||
| 393 | }), | ||
| 394 | ); | ||
| 395 | |||
| 376 | it.effect("added OpenAPI source can be listed, inspected, and invoked through execution", () => | 396 | it.effect("added OpenAPI source can be listed, inspected, and invoked through execution", () => |
| 377 | Effect.gen(function* () { | 397 | Effect.gen(function* () { |
| 378 | const server = yield* Effect.acquireRelease( | 398 | const server = yield* Effect.acquireRelease( |
| @@ -3,9 +3,9 @@ · 2 unmodified lines | |||
| 3 | // on the full cloud module graph. | 3 | // on the full cloud module graph. |
| 4 | 4 | ||
| 5 | import { describe, expect, it } from "@effect/vitest"; | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 6 | import { Effect } from "effect"; | 6 | import { Effect, Result } from "effect"; |
| 7 | 7 | ||
| 8 | import { ScopeId, SecretId } from "@executor-js/sdk"; | 8 | import { ConnectionId, ScopeId, SecretId } from "@executor-js/sdk"; |
| 9 | 9 | ||
| 10 | import { asOrg } from "./__test-harness__/api-harness"; | 10 | import { asOrg } from "./__test-harness__/api-harness"; |
| 11 | 11 | ||
| @@ -23,7 +23,80 @@ const MINIMAL_OPENAPI_SPEC = JSON.stringify({ · 11 unmodified lines | |||
| 23 | }); | 23 | }); |
| 24 | 24 | ||
| 25 | describe("tenant isolation (HTTP)", () => { | 25 | describe("tenant isolation (HTTP)", () => { |
| 26 | it.effect("sources.list does not leak across orgs", () => | 26 | it.effect("write requests cannot target another org scope", () => |
| 27 | Effect.gen(function* () { | ||
| 28 | const orgA = `org_${crypto.randomUUID()}`; | ||
| 29 | const orgB = `org_${crypto.randomUUID()}`; | ||
| 30 | |||
| 31 | const result = yield* asOrg(orgB, (client) => | ||
| 32 | client.secrets.set({ | ||
| 33 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 34 | payload: { | ||
| 35 | id: SecretId.make("planted"), | ||
| 36 | name: "planted-by-org-b", | ||
| 37 | value: "should-be-rejected", | ||
| 38 | }, | ||
| 39 | }), | ||
| 40 | ).pipe(Effect.result); | ||
| 41 | |||
| 42 | expect(Result.isFailure(result)).toBe(true); | ||
| 43 | |||
| 44 | const orgASecrets = yield* asOrg(orgA, (client) => | ||
| 45 | client.secrets.list({ params: { scopeId: ScopeId.make(orgA) } }), | ||
| 46 | ); | ||
| 47 | expect(orgASecrets.map((s) => s.id)).not.toContain("planted"); | ||
| 48 | }), | ||
| 49 | ); | ||
| 50 | |||
| 51 | it.effect("read requests with another org scope still use the caller stack", () => | ||
| 52 | Effect.gen(function* () { | ||
| 53 | const orgA = `org_${crypto.randomUUID()}`; | ||
| 54 | const orgB = `org_${crypto.randomUUID()}`; | ||
| 55 | const idA = `sec_a_${crypto.randomUUID().slice(0, 8)}`; | ||
| 56 | |||
| 57 | yield* asOrg(orgA, (client) => | ||
| 58 | client.secrets.set({ | ||
| 59 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 60 | payload: { id: SecretId.make(idA), name: "org-a only", value: "v" }, | ||
| 61 | }), | ||
| 62 | ); | ||
| 63 | |||
| 64 | const result = yield* asOrg(orgB, (client) => | ||
| 65 | client.secrets.list({ params: { scopeId: ScopeId.make(orgA) } }), | ||
| 66 | ); | ||
| 67 | expect(result.map((s) => s.id)).not.toContain(idA); | ||
| 68 | }), | ||
| 69 | ); | ||
| 70 | |||
| 71 | it.effect("delete requests cannot remove another org secret", () => | ||
| 72 | Effect.gen(function* () { | ||
| 73 | const orgA = `org_${crypto.randomUUID()}`; | ||
| 74 | const orgB = `org_${crypto.randomUUID()}`; | ||
| 75 | const idA = `sec_a_${crypto.randomUUID().slice(0, 8)}`; | ||
| 76 | |||
| 77 | yield* asOrg(orgA, (client) => | ||
| 78 | client.secrets.set({ | ||
| 79 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 80 | payload: { id: SecretId.make(idA), name: "org-a only", value: "v" }, | ||
| 81 | }), | ||
| 82 | ); | ||
| 83 | |||
| 84 | yield* asOrg(orgB, (client) => | ||
| 85 | client.secrets.remove({ | ||
| 86 | params: { scopeId: ScopeId.make(orgA), secretId: SecretId.make(idA) }, | ||
| 87 | }), | ||
| 88 | ).pipe(Effect.result); | ||
| 89 | |||
| 90 | const status = yield* asOrg(orgA, (client) => | ||
| 91 | client.secrets.status({ | ||
| 92 | params: { scopeId: ScopeId.make(orgA), secretId: SecretId.make(idA) }, | ||
| 93 | }), | ||
| 94 | ); | ||
| 95 | expect(status.status).toBe("resolved"); | ||
| 96 | }), | ||
| 97 | ); | ||
| 98 | |||
| 99 | it.effect("sources.list is scoped to the caller org", () => | ||
| 27 | Effect.gen(function* () { | 100 | Effect.gen(function* () { |
| 28 | const orgA = `org_${crypto.randomUUID()}`; | 101 | const orgA = `org_${crypto.randomUUID()}`; |
| 29 | const orgB = `org_${crypto.randomUUID()}`; | 102 | const orgB = `org_${crypto.randomUUID()}`; |
| @@ -47,7 +120,7 @@ describe("tenant isolation (HTTP)", () => { · 17 unmodified lines | |||
| 47 | }), | 120 | }), |
| 48 | ); | 121 | ); |
| 49 | 122 | ||
| 50 | it.effect("tools.list does not leak across orgs", () => | 123 | it.effect("tools.list is scoped to the caller org", () => |
| 51 | Effect.gen(function* () { | 124 | Effect.gen(function* () { |
| 52 | const orgA = `org_${crypto.randomUUID()}`; | 125 | const orgA = `org_${crypto.randomUUID()}`; |
| 53 | const orgB = `org_${crypto.randomUUID()}`; | 126 | const orgB = `org_${crypto.randomUUID()}`; |
| @@ -68,6 +141,9 @@ describe("tenant isolation (HTTP)", () => { · 14 unmodified lines | |||
| 68 | client.tools.list({ params: { scopeId: ScopeId.make(orgB) } }), | 141 | client.tools.list({ params: { scopeId: ScopeId.make(orgB) } }), |
| 69 | ); | 142 | ); |
| 70 | expect(orgBTools.map((t) => t.sourceId)).not.toContain(namespaceA); | 143 | expect(orgBTools.map((t) => t.sourceId)).not.toContain(namespaceA); |
| 144 | for (const id of orgBTools.map((t) => t.id)) { | ||
| 145 | expect(id).not.toContain(namespaceA); | ||
| 146 | } | ||
| 71 | }), | 147 | }), |
| 72 | ); | 148 | ); |
| 73 | 149 | ||
| @@ -98,7 +174,7 @@ describe("tenant isolation (HTTP)", () => { · 24 unmodified lines | |||
| 98 | }), | 174 | }), |
| 99 | ); | 175 | ); |
| 100 | 176 | ||
| 101 | it.effect("secrets.list does not leak across orgs", () => | 177 | it.effect("secrets.list is scoped to the caller org", () => |
| 102 | Effect.gen(function* () { | 178 | Effect.gen(function* () { |
| 103 | const orgA = `org_${crypto.randomUUID()}`; | 179 | const orgA = `org_${crypto.randomUUID()}`; |
| 104 | const orgB = `org_${crypto.randomUUID()}`; | 180 | const orgB = `org_${crypto.randomUUID()}`; |
| @@ -168,6 +244,104 @@ describe("tenant isolation (HTTP)", () => { · 63 unmodified lines | |||
| 168 | }), | 244 | }), |
| 169 | ); | 245 | ); |
| 170 | 246 | ||
| 247 | it.effect("secret usages are scoped to the caller stack", () => | ||
| 248 | Effect.gen(function* () { | ||
| 249 | const orgA = `org_${crypto.randomUUID()}`; | ||
| 250 | const orgB = `org_${crypto.randomUUID()}`; | ||
| 251 | const namespaceA = `a_${crypto.randomUUID().replace(/-/g, "_")}`; | ||
| 252 | const secretIdA = SecretId.make(`sec_a_${crypto.randomUUID().slice(0, 8)}`); | ||
| 253 | |||
| 254 | yield* asOrg(orgA, (client) => | ||
| 255 | Effect.gen(function* () { | ||
| 256 | yield* client.secrets.set({ | ||
| 257 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 258 | payload: { id: secretIdA, name: "org-a token", value: "v" }, | ||
| 259 | }); | ||
| 260 | yield* client.openapi.addSpec({ | ||
| 261 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 262 | payload: { | ||
| 263 | targetScope: ScopeId.make(orgA), | ||
| 264 | spec: MINIMAL_OPENAPI_SPEC, | ||
| 265 | namespace: namespaceA, | ||
| 266 | headers: { | ||
| 267 | Authorization: { | ||
| 268 | kind: "binding", | ||
| 269 | slot: "auth:token", | ||
| 270 | prefix: "Bearer ", | ||
| 271 | }, | ||
| 272 | }, | ||
| 273 | }, | ||
| 274 | }); | ||
| 275 | yield* client.openapi.setSourceBinding({ | ||
| 276 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 277 | payload: { | ||
| 278 | sourceId: namespaceA, | ||
| 279 | sourceScope: ScopeId.make(orgA), | ||
| 280 | scope: ScopeId.make(orgA), | ||
| 281 | slot: "auth:token", | ||
| 282 | value: { kind: "secret", secretId: secretIdA }, | ||
| 283 | }, | ||
| 284 | }); | ||
| 285 | }), | ||
| 286 | ); | ||
| 287 | |||
| 288 | const usages = yield* asOrg(orgB, (client) => | ||
| 289 | client.secrets.usages({ | ||
| 290 | params: { scopeId: ScopeId.make(orgB), secretId: secretIdA }, | ||
| 291 | }), | ||
| 292 | ); | ||
| 293 | |||
| 294 | expect(usages).toEqual([]); | ||
| 295 | }), | ||
| 296 | ); | ||
| 297 | |||
| 298 | it.effect("connection usages are scoped to the caller stack", () => | ||
| 299 | Effect.gen(function* () { | ||
| 300 | const orgA = `org_${crypto.randomUUID()}`; | ||
| 301 | const orgB = `org_${crypto.randomUUID()}`; | ||
| 302 | const namespaceA = `a_${crypto.randomUUID().replace(/-/g, "_")}`; | ||
| 303 | const connectionIdA = ConnectionId.make(`conn_a_${crypto.randomUUID().slice(0, 8)}`); | ||
| 304 | |||
| 305 | yield* asOrg(orgA, (client) => | ||
| 306 | Effect.gen(function* () { | ||
| 307 | yield* client.openapi.addSpec({ | ||
| 308 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 309 | payload: { | ||
| 310 | targetScope: ScopeId.make(orgA), | ||
| 311 | spec: MINIMAL_OPENAPI_SPEC, | ||
| 312 | namespace: namespaceA, | ||
| 313 | headers: { | ||
| 314 | Authorization: { | ||
| 315 | kind: "binding", | ||
| 316 | slot: "auth:conn", | ||
| 317 | prefix: "Bearer ", | ||
| 318 | }, | ||
| 319 | }, | ||
| 320 | }, | ||
| 321 | }); | ||
| 322 | yield* client.openapi.setSourceBinding({ | ||
| 323 | params: { scopeId: ScopeId.make(orgA) }, | ||
| 324 | payload: { | ||
| 325 | sourceId: namespaceA, | ||
| 326 | sourceScope: ScopeId.make(orgA), | ||
| 327 | scope: ScopeId.make(orgA), | ||
| 328 | slot: "auth:conn", | ||
| 329 | value: { kind: "connection", connectionId: connectionIdA }, | ||
| 330 | }, | ||
| 331 | }); | ||
| 332 | }), | ||
| 333 | ).pipe(Effect.result); | ||
| 334 | |||
| 335 | const usages = yield* asOrg(orgB, (client) => | ||
| 336 | client.connections.usages({ | ||
| 337 | params: { scopeId: ScopeId.make(orgB), connectionId: connectionIdA }, | ||
| 338 | }), | ||
| 339 | ).pipe(Effect.result); | ||
| 340 | |||
| 341 | if (Result.isSuccess(usages)) expect(usages.success).toEqual([]); | ||
| 342 | }), | ||
| 343 | ); | ||
| 344 | |||
| 171 | it.effect("updating a same-namespace OpenAPI source in one org does not mutate another org", () => | 345 | it.effect("updating a same-namespace OpenAPI source in one org does not mutate another org", () => |
| 172 | Effect.gen(function* () { | 346 | Effect.gen(function* () { |
| 173 | const orgA = `org_${crypto.randomUUID()}`; | 347 | const orgA = `org_${crypto.randomUUID()}`; |