// Cloud-only: the OWNER's view of a workspace — `/api/admin/users*`. The // product plane answers "what can I, this member, reach"; this plane answers // "who uses my workspace, and what have they connected", reading across every // member of the tenant instead of binding to one. // // Two members are built through the REAL flows (login → create-organization → // invite → accept-invitation), each connects their own credential, and the // admin then reads the joined view — the exact shape a customer dashboard's // icon grid consumes. The guarantees pinned here: // // 1. the joined view reports BOTH members and each one's own connections, // even though no single product-view caller can see another member's; // 2. it never carries credential material; // 3. a plain member is refused (403) and an anonymous caller too (401); // 4. another tenant's admin sees none of it. // // EMULATOR GAP: the WorkOS emulator hardcodes `owner.type: "user"` in its api-key // serializer and exposes no organization-key mint route, so an org-scoped API // key — the machine credential for this plane — cannot exist end-to-end here. // This scenario therefore drives the ADMIN SESSION path, which is the other // credential the plane accepts. The org-key half is covered at unit level in // `apps/cloud/src/auth/org-api-key-mint.node.test.ts` (against the real WorkOS // response shape) and `org-api-key-auth.node.test.ts` (PlatformAuth resolution). import { randomBytes } from "node:crypto"; import { expect } from "@effect/vitest"; import { Effect } from "effect"; import type { HttpApiClient } from "effect/unstable/httpapi"; import { AdminUsersHttpApi } from "@executor-js/api"; import { composePluginApi } from "@executor-js/api/server"; import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario"; import { Api, Target } from "../src/services"; import type { Identity, Target as TargetShape } from "../src/target"; const api = composePluginApi([openApiHttpPlugin()] as const); type Client = HttpApiClient.ForApi; const TEMPLATE_API_KEY = AuthTemplateSlug.make("apiKey"); /** Minimal OpenAPI spec with a single GET /ping — never contacted here. */ const pingSpec = JSON.stringify({ openapi: "3.0.3", info: { title: "Ping API", version: "1.0.0" }, paths: { "/ping": { get: { operationId: "ping", summary: "Ping", responses: { "200": { description: "pong" } } }, }, }, }); /** Registers a fresh apiKey-authenticated integration for connections to bind to. */ const registerIntegration = (client: Client) => Effect.gen(function* () { const slug = IntegrationSlug.make(`admin-scn-${randomBytes(4).toString("hex")}`); yield* client.openapi.addSpec({ payload: { spec: { kind: "blob", value: pingSpec }, slug, baseUrl: "http://127.0.0.1:59999", // never contacted during registration authenticationTemplate: [ { slug: "apiKey", type: "apiKey", headers: { authorization: ["Bearer ", { type: "variable", name: "token" }] }, }, ], }, }); return slug; }); const freshConnectionName = () => ConnectionName.make(`conn${randomBytes(4).toString("hex")}`); const cookieOf = (identity: Identity): string => identity.headers?.["cookie"] ?? ""; const postJson = (target: TargetShape, path: string, identity: Identity, body: unknown) => Effect.promise(async () => { const response = await fetch(new URL(path, target.baseUrl), { method: "POST", headers: { "content-type": "application/json", origin: new URL(target.baseUrl).origin, cookie: cookieOf(identity), }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`${path} failed (${response.status}): ${await response.text()}`); } return response; }); /** The identity re-bound to the refreshed session cookie a response set. */ const withRefreshedSession = (identity: Identity, response: Response): Identity => { const refreshed = (response.headers.getSetCookie?.() ?? []) .find((header) => header.startsWith("wos-session=")) ?.split(";")[0]; if (!refreshed) throw new Error("response did not refresh the session cookie"); return { ...identity, headers: { cookie: refreshed } }; }; /** Invite `member` into `admin`'s org and accept — the real invite flow. */ const joinOrg = (target: TargetShape, admin: Identity, member: Identity) => Effect.gen(function* () { const inviteResponse = yield* postJson(target, "/api/account/members/invite", admin, { email: member.credentials?.email, }); const invitation = (yield* Effect.promise(() => inviteResponse.json())) as { id: string }; const acceptResponse = yield* postJson(target, "/api/auth/accept-invitation", member, { invitationId: invitation.id, }); return withRefreshedSession(member, acceptResponse); }); /** The caller's own account id — the `externalId` the admin plane reports. */ const accountIdOf = (target: TargetShape, identity: Identity) => Effect.promise(async () => { const response = await fetch(new URL("/api/account/me", target.baseUrl), { headers: { cookie: cookieOf(identity) }, }); if (!response.ok) throw new Error(`/api/account/me failed (${response.status})`); const body = (await response.json()) as { user: { id: string } }; return body.user.id; }); scenario( "Admin · the owner sees every member of the workspace and what each has connected", {}, Effect.gen(function* () { const target = yield* Target; const { client: apiClient } = yield* Api; const admin = yield* target.newIdentity(); const invitee = yield* target.newIdentity({ org: false }); const member = yield* joinOrg(target, admin, invitee); const adminClient = yield* apiClient(api, admin); const memberClient = yield* apiClient(api, member); const adminId = yield* accountIdOf(target, admin); const memberId = yield* accountIdOf(target, member); const integration = yield* registerIntegration(adminClient); const adminConnection = freshConnectionName(); const memberConnection = freshConnectionName(); yield* Effect.ensuring( Effect.gen(function* () { // Each member stores their OWN credential. Neither can see the other's // through the product plane — that is the whole point of the admin one. yield* adminClient.connections.create({ payload: { owner: "user", name: adminConnection, integration, template: TEMPLATE_API_KEY, value: "admin-personal-token", }, }); yield* memberClient.connections.create({ payload: { owner: "user", name: memberConnection, integration, template: TEMPLATE_API_KEY, value: "member-personal-token", }, }); const client = yield* apiClient(AdminUsersHttpApi, admin); // (1) The joined view: both members, each with their own connection. const joined = yield* client.adminUsers.listUsersWithConnections({ query: {} }); const byId = new Map(joined.users.map((user) => [user.externalId, user])); expect([...byId.keys()].sort(), "both members of the workspace are reported").toEqual( [adminId, memberId].sort(), ); expect( byId.get(adminId)?.connections.map((connection) => connection.name), "the admin's own connection is attributed to them", ).toContain(adminConnection); expect( byId.get(memberId)?.connections.map((connection) => connection.name), "the member's connection is visible to the owner, though not to the admin's product view", ).toContain(memberConnection); // The flat list agrees with the joined one. const flat = yield* client.adminUsers.listUsers({ query: {} }); expect( flat.users.map((user) => user.externalId).sort(), "the flat list reports the same members", ).toEqual([adminId, memberId].sort()); // Per-user connections resolve the same rows. const memberConnections = yield* client.adminUsers.listUserConnections({ params: { externalId: memberId }, }); expect( memberConnections.connections.map((connection) => connection.name), "the member's connections are readable by external id", ).toContain(memberConnection); // (2) No credential material anywhere in the joined view. expect( JSON.stringify(joined), "the admin view never carries a stored credential", ).not.toContain("member-personal-token"); expect(JSON.stringify(joined)).not.toContain("admin-personal-token"); // (3) A plain member is refused: this plane reports on everyone. const asMember = yield* Effect.promise(() => fetch(new URL("/api/admin/users", target.baseUrl), { headers: { cookie: cookieOf(member) }, }), ); expect(asMember.status, "a non-admin member cannot read the workspace view").toBe(403); // And an anonymous caller has no credential at all. const anonymous = yield* Effect.promise(() => fetch(new URL("/api/admin/users", target.baseUrl)), ); expect(anonymous.status, "no session → unauthorized").toBe(401); }), Effect.all( [ adminClient.connections .remove({ params: { owner: "user", integration, name: adminConnection } }) .pipe(Effect.ignore), memberClient.connections .remove({ params: { owner: "user", integration, name: memberConnection } }) .pipe(Effect.ignore), adminClient.openapi.removeSpec({ params: { slug: integration } }).pipe(Effect.ignore), ], { discard: true }, ), ); }), ); scenario( "Admin · one workspace's owner sees nothing of another workspace", {}, Effect.gen(function* () { const target = yield* Target; const { client: apiClient } = yield* Api; const mine = yield* target.newIdentity(); const theirs = yield* target.newIdentity(); const myId = yield* accountIdOf(target, mine); const theirId = yield* accountIdOf(target, theirs); const myClient = yield* apiClient(api, mine); // A user earns their row in the workspace the first time they touch the // EXECUTOR plane (the account plane doesn't bind an executor), so the other // owner makes one product read — otherwise their own workspace is empty and // the isolation assertion below would pass vacuously. yield* (yield* apiClient(api, theirs)).connections.list({ query: {} }); const integration = yield* registerIntegration(myClient); const connection = freshConnectionName(); yield* Effect.ensuring( Effect.gen(function* () { yield* myClient.connections.create({ payload: { owner: "user", name: connection, integration, template: TEMPLATE_API_KEY, value: "my-private-token", }, }); const theirAdmin = yield* apiClient(AdminUsersHttpApi, theirs); const theirView = yield* theirAdmin.adminUsers.listUsersWithConnections({ query: {} }); expect( theirView.users.map((user) => user.externalId), "another workspace's owner never sees my members", ).not.toContain(myId); expect( theirView.users.map((user) => user.externalId), "they do see their own", ).toContain(theirId); expect( JSON.stringify(theirView), "and none of my connections leak across the tenant boundary", ).not.toContain(connection); // Naming my account id directly returns nothing rather than my rows. const probe = yield* theirAdmin.adminUsers.listUserConnections({ params: { externalId: myId }, }); expect(probe.connections, "a cross-tenant id resolves to nothing").toEqual([]); }), Effect.all( [ myClient.connections .remove({ params: { owner: "user", integration, name: connection } }) .pipe(Effect.ignore), myClient.openapi.removeSpec({ params: { slug: integration } }).pipe(Effect.ignore), ], { discard: true }, ), ); }), );