From 5199a486413afd081b05636714a426713e694cf9 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Sun, 3 May 2026 23:12:57 -0700 Subject: [PATCH] Cut over scope ids to deterministic prefixed form Org and user-org scope rows now address as org_ and user_org__ respectively. The 0009 migration rewrites every scope_id column on every scoped table in one shot; runtime construction in apps/cloud/src/services/executor.ts now goes through orgScopeId / userOrgScopeId from ./ids, and the test harness exports the same helpers so test code never has to spell the format inline. --- apps/cloud/drizzle/0009_scope_id_prefix.sql | 48 + apps/cloud/drizzle/meta/0009_snapshot.json | 1755 +++++++++++++++++ apps/cloud/drizzle/meta/_journal.json | 7 + .../src/secrets-isolation.e2e.node.test.ts | 35 +- .../services/__test-harness__/api-harness.ts | 13 +- apps/cloud/src/services/executor.ts | 16 +- .../src/services/secrets-api.node.test.ts | 43 +- .../src/services/sources-api.node.test.ts | 83 +- .../src/services/sources-refresh.node.test.ts | 22 +- .../services/tenant-isolation.node.test.ts | 40 +- 10 files changed, 1936 insertions(+), 126 deletions(-) create mode 100644 apps/cloud/drizzle/0009_scope_id_prefix.sql create mode 100644 apps/cloud/drizzle/meta/0009_snapshot.json diff --git a/apps/cloud/drizzle/0009_scope_id_prefix.sql b/apps/cloud/drizzle/0009_scope_id_prefix.sql new file mode 100644 index 000000000..db82e4fef --- /dev/null +++ b/apps/cloud/drizzle/0009_scope_id_prefix.sql @@ -0,0 +1,48 @@ +-- --------------------------------------------------------------------------- +-- One-shot migration: convert raw scope ids to deterministic prefixed form. +-- +-- -> org_ +-- user-org:: -> user_org__ +-- +-- Mirrors orgScopeId() / userOrgScopeId() in apps/cloud/src/services/ids.ts. +-- Run order: rewrite user-org rows first so they don't accidentally match +-- the org rewrite (a `user-org:` value will never be an organizations.id, +-- but keeping the rules independent of catalog state is cheaper than +-- proving it). +-- --------------------------------------------------------------------------- + +-- user-org:U:O -> user_org_U_O across every scoped table. +UPDATE "source" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "tool" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "definition" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "secret" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "connection" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "oauth2_session" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "tool_policy" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "openapi_source" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "openapi_operation" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "openapi_source_binding" SET "source_scope_id" = 'user_org_' || replace(substring("source_scope_id" from 10), ':', '_') WHERE "source_scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "openapi_source_binding" SET "target_scope_id" = 'user_org_' || replace(substring("target_scope_id" from 10), ':', '_') WHERE "target_scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "mcp_source" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "mcp_binding" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "graphql_source" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "graphql_operation" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint +UPDATE "workos_vault_metadata" SET "scope_id" = 'user_org_' || replace(substring("scope_id" from 10), ':', '_') WHERE "scope_id" LIKE 'user-org:%';--> statement-breakpoint + +-- raw -> org_ for every scoped table. +UPDATE "source" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "tool" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "definition" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "secret" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "connection" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "oauth2_session" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "tool_policy" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "openapi_source" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "openapi_operation" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "openapi_source_binding" SET "source_scope_id" = 'org_' || "source_scope_id" WHERE "source_scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "openapi_source_binding" SET "target_scope_id" = 'org_' || "target_scope_id" WHERE "target_scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "mcp_source" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "mcp_binding" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "graphql_source" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "graphql_operation" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations");--> statement-breakpoint +UPDATE "workos_vault_metadata" SET "scope_id" = 'org_' || "scope_id" WHERE "scope_id" IN (SELECT "id" FROM "organizations"); diff --git a/apps/cloud/drizzle/meta/0009_snapshot.json b/apps/cloud/drizzle/meta/0009_snapshot.json new file mode 100644 index 000000000..b7df1765b --- /dev/null +++ b/apps/cloud/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1755 @@ +{ + "id": "29c4960b-a264-4c03-b6dd-d279aba5e310", + "prevId": "f521e4d8-1eb4-4f84-8110-38fb5157aaca", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_account_id_organization_id_pk": { + "name": "memberships_account_id_organization_id_pk", + "columns": [ + "account_id", + "organization_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_handle_unique": { + "name": "organizations_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "handle" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_organization_slug_unique": { + "name": "workspaces_organization_slug_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_organization_id_idx": { + "name": "workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_organization_id_organizations_id_fk": { + "name": "workspaces_organization_id_organizations_id_fk", + "tableFrom": "workspaces", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob": { + "name": "blob", + "schema": "", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "blob_namespace_key_pk": { + "name": "blob_namespace_key_pk", + "columns": [ + "namespace", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection": { + "name": "connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_secret_id": { + "name": "access_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_secret_id": { + "name": "refresh_token_secret_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_state": { + "name": "provider_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_scope_id_idx": { + "name": "connection_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connection_provider_idx": { + "name": "connection_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "connection_scope_id_id_pk": { + "name": "connection_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.definition": { + "name": "definition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "definition_scope_id_idx": { + "name": "definition_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "definition_source_id_idx": { + "name": "definition_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "definition_plugin_id_idx": { + "name": "definition_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "definition_scope_id_id_pk": { + "name": "definition_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_operation": { + "name": "graphql_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "graphql_operation_scope_id_idx": { + "name": "graphql_operation_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "graphql_operation_source_id_idx": { + "name": "graphql_operation_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_operation_scope_id_id_pk": { + "name": "graphql_operation_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.graphql_source": { + "name": "graphql_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "query_params": { + "name": "query_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "auth": { + "name": "auth", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "graphql_source_scope_id_idx": { + "name": "graphql_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "graphql_source_scope_id_id_pk": { + "name": "graphql_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_binding": { + "name": "mcp_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_binding_scope_id_idx": { + "name": "mcp_binding_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_binding_source_id_idx": { + "name": "mcp_binding_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_binding_scope_id_id_pk": { + "name": "mcp_binding_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_source": { + "name": "mcp_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "mcp_source_scope_id_idx": { + "name": "mcp_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "mcp_source_scope_id_id_pk": { + "name": "mcp_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth2_session": { + "name": "oauth2_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy": { + "name": "strategy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_scope": { + "name": "token_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth2_session_scope_id_idx": { + "name": "oauth2_session_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth2_session_plugin_id_idx": { + "name": "oauth2_session_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth2_session_connection_id_idx": { + "name": "oauth2_session_connection_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "oauth2_session_scope_id_id_pk": { + "name": "oauth2_session_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_operation": { + "name": "openapi_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding": { + "name": "binding", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "openapi_operation_scope_id_idx": { + "name": "openapi_operation_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_operation_source_id_idx": { + "name": "openapi_operation_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_operation_scope_id_id_pk": { + "name": "openapi_operation_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source": { + "name": "openapi_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "query_params": { + "name": "query_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "oauth2": { + "name": "oauth2", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "invocation_config": { + "name": "invocation_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "openapi_source_scope_id_idx": { + "name": "openapi_source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "openapi_source_scope_id_id_pk": { + "name": "openapi_source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.openapi_source_binding": { + "name": "openapi_source_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_scope_id": { + "name": "source_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_scope_id": { + "name": "target_scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slot": { + "name": "slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "openapi_source_binding_source_id_idx": { + "name": "openapi_source_binding_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_source_scope_id_idx": { + "name": "openapi_source_binding_source_scope_id_idx", + "columns": [ + { + "expression": "source_scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_target_scope_id_idx": { + "name": "openapi_source_binding_target_scope_id_idx", + "columns": [ + { + "expression": "target_scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "openapi_source_binding_slot_idx": { + "name": "openapi_source_binding_slot_idx", + "columns": [ + { + "expression": "slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret": { + "name": "secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owned_by_connection_id": { + "name": "owned_by_connection_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "secret_scope_id_idx": { + "name": "secret_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_provider_idx": { + "name": "secret_provider_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_owned_by_connection_id_idx": { + "name": "secret_owned_by_connection_id_idx", + "columns": [ + { + "expression": "owned_by_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "secret_scope_id_id_pk": { + "name": "secret_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.source": { + "name": "source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "can_remove": { + "name": "can_remove", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "can_edit": { + "name": "can_edit", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "source_scope_id_idx": { + "name": "source_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "source_plugin_id_idx": { + "name": "source_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "source_scope_id_id_pk": { + "name": "source_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_scope_id_idx": { + "name": "tool_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_source_id_idx": { + "name": "tool_source_id_idx", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tool_plugin_id_idx": { + "name": "tool_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_scope_id_id_pk": { + "name": "tool_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policy": { + "name": "tool_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_policy_scope_id_position_idx": { + "name": "tool_policy_scope_id_position_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "tool_policy_scope_id_id_pk": { + "name": "tool_policy_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workos_vault_metadata": { + "name": "workos_vault_metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workos_vault_metadata_scope_id_idx": { + "name": "workos_vault_metadata_scope_id_idx", + "columns": [ + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workos_vault_metadata_scope_id_id_pk": { + "name": "workos_vault_metadata_scope_id_id_pk", + "columns": [ + "scope_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json index f3b5b7c4f..1d94f4a7d 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1777874366489, "tag": "0008_stiff_radioactive_man", "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1777874400000, + "tag": "0009_scope_id_prefix", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/cloud/src/secrets-isolation.e2e.node.test.ts b/apps/cloud/src/secrets-isolation.e2e.node.test.ts index 64773bf66..6bcbe81f1 100644 --- a/apps/cloud/src/secrets-isolation.e2e.node.test.ts +++ b/apps/cloud/src/secrets-isolation.e2e.node.test.ts @@ -29,10 +29,11 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; -import { ScopeId, SecretId } from "@executor-js/sdk"; +import { SecretId } from "@executor-js/sdk"; import { asUser, + orgScopeId, testUserOrgScopeId, } from "./services/__test-harness__/api-harness"; @@ -53,7 +54,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { yield* asUser(alice, orgA, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { id: SecretId.make(id), name: "Shared", @@ -64,13 +65,13 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const charlieStatus = yield* asUser(charlie, orgB, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(orgB), secretId: SecretId.make(id) }, }), ); expect(charlieStatus.status).toBe("missing"); const charlieList = yield* asUser(charlie, orgB, (client) => - client.secrets.list({ params: { scopeId: ScopeId.make(orgB) } }), + client.secrets.list({ params: { scopeId: orgScopeId(orgB) } }), ); expect(charlieList.map((s) => s.id)).not.toContain(id); @@ -89,7 +90,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { // Alice writes at her per-user scope — where OAuth tokens land. yield* asUser(aliceId, orgId, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)) }, + params: { scopeId: testUserOrgScopeId(aliceId, orgId) }, payload: { id: SecretId.make(id), name: "Alice's token", @@ -102,7 +103,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { // not see the token in a list. const bobList = yield* asUser(bobId, orgId, (client) => client.secrets.list({ - params: { scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)) }, + params: { scopeId: testUserOrgScopeId(bobId, orgId) }, }), ); expect(bobList.map((s) => s.id)).not.toContain(id); @@ -110,7 +111,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const bobStatus = yield* asUser(bobId, orgId, (client) => client.secrets.status({ params: { - scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)), + scopeId: testUserOrgScopeId(bobId, orgId), secretId: SecretId.make(id), }, }), @@ -121,7 +122,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const aliceStatus = yield* asUser(aliceId, orgId, (client) => client.secrets.status({ params: { - scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)), + scopeId: testUserOrgScopeId(aliceId, orgId), secretId: SecretId.make(id), }, }), @@ -141,7 +142,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { yield* asUser(adminId, orgId, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(orgId) }, + params: { scopeId: orgScopeId(orgId) }, payload: { id: SecretId.make(id), name: "Org API Key", @@ -152,12 +153,12 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const adminStatus = yield* asUser(adminId, orgId, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(orgId), secretId: SecretId.make(id) }, }), ); const memberStatus = yield* asUser(memberId, orgId, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(orgId), secretId: SecretId.make(id) }, }), ); expect(adminStatus.status).toBe("resolved"); @@ -176,7 +177,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { yield* asUser(userId, orgA, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)) }, + params: { scopeId: testUserOrgScopeId(userId, orgA) }, payload: { id: SecretId.make(id), name: "A token", @@ -190,7 +191,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { // logs into org B. const listInB = yield* asUser(userId, orgB, (client) => client.secrets.list({ - params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)) }, + params: { scopeId: testUserOrgScopeId(userId, orgB) }, }), ); expect(listInB.map((s) => s.id)).not.toContain(id); @@ -198,7 +199,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const statusInB = yield* asUser(userId, orgB, (client) => client.secrets.status({ params: { - scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)), + scopeId: testUserOrgScopeId(userId, orgB), secretId: SecretId.make(id), }, }), @@ -210,7 +211,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const statusInA = yield* asUser(userId, orgA, (client) => client.secrets.status({ params: { - scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)), + scopeId: testUserOrgScopeId(userId, orgA), secretId: SecretId.make(id), }, }), @@ -228,7 +229,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const result = yield* asUser(userId, orgId, (client) => client.secrets .set({ - params: { scopeId: ScopeId.make(foreignOrg) }, + params: { scopeId: orgScopeId(foreignOrg) }, payload: { id: SecretId.make("wrong-scope"), name: "x", @@ -245,7 +246,7 @@ describe("cloud secret isolation (HTTP, user-org scope stack)", () => { const leaked = yield* asUser(foreignUser, foreignOrg, (client) => client.secrets.status({ params: { - scopeId: ScopeId.make(foreignOrg), + scopeId: orgScopeId(foreignOrg), secretId: SecretId.make("wrong-scope"), }, }), diff --git a/apps/cloud/src/services/__test-harness__/api-harness.ts b/apps/cloud/src/services/__test-harness__/api-harness.ts index 4ae4d5ff5..9985d6d19 100644 --- a/apps/cloud/src/services/__test-harness__/api-harness.ts +++ b/apps/cloud/src/services/__test-harness__/api-harness.ts @@ -33,7 +33,6 @@ import { createExecutionEngine } from "@executor-js/execution"; import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs"; import { Scope, - ScopeId, collectSchemas, createExecutor, } from "@executor-js/sdk"; @@ -51,17 +50,12 @@ import { RouterConfig, } from "../../api/protected-layers"; import { DbService } from "../db"; +import { orgScopeId, userOrgScopeId } from "../ids"; export const TEST_BASE_URL = "http://test.local"; export const TEST_ORG_HEADER = "x-test-org-id"; export const TEST_USER_HEADER = "x-test-user-id"; -// Mirrors apps/cloud/src/services/executor.ts#createScopedExecutor — the -// per-user scope id bakes in the org so the same user id in a different -// org gets a distinct scope row. -const userOrgScopeId = (userId: string, orgId: string) => - `user-org:${userId}:${orgId}`; - // `asOrg(orgId, …)` callers don't care which specific user they are, only // that the executor has a valid user-org scope. We give each org a stable // default user so list/get operations at the org scope remain deterministic @@ -89,12 +83,12 @@ const createTestScopedExecutor = ( const adapter = makePostgresAdapter({ db, schema }); const blobs = makePostgresBlobStore({ db }); const orgScope = new Scope({ - id: ScopeId.make(orgId), + id: orgScopeId(orgId), name: orgName, createdAt: new Date(), }); const userOrgScope = new Scope({ - id: ScopeId.make(userOrgScopeId(userId, orgId)), + id: userOrgScopeId(userId, orgId), name: `Personal · ${orgName}`, createdAt: new Date(), }); @@ -252,3 +246,4 @@ export const testUserOrgScopeId = (userId: string, orgId: string) => // Re-exports so call sites don't need a second import. export { ProtectedCloudApi }; +export { orgScopeId, userOrgScopeId }; diff --git a/apps/cloud/src/services/executor.ts b/apps/cloud/src/services/executor.ts index 33d9d8f54..a32f8c2dd 100644 --- a/apps/cloud/src/services/executor.ts +++ b/apps/cloud/src/services/executor.ts @@ -12,7 +12,6 @@ import { Effect } from "effect"; import { Scope, - ScopeId, collectSchemas, createExecutor, } from "@executor-js/sdk"; @@ -24,6 +23,7 @@ import { import { env } from "cloudflare:workers"; import executorConfig from "../../executor.config"; import { DbService } from "./db"; +import { orgScopeId, userOrgScopeId } from "./ids"; // --------------------------------------------------------------------------- // Plugin list lives in `executor.config.ts` — that file is the single @@ -45,11 +45,11 @@ const orgPlugins = (): CloudPlugins => // --------------------------------------------------------------------------- // Create a fresh executor for a (user, org) pair (stateless, per-request). // -// Scope stack is `[userOrgScope, orgScope]` — innermost first. The -// user-within-org scope id (`user-org:${userId}:${orgId}`) intentionally -// includes the org id so the same WorkOS user in a different org gets a -// distinct scope row; future workspace scopes can slot in between without -// conflicting with a hypothetical global user scope. +// Scope stack is `[userOrgScope, orgScope]` — innermost first. Scope ids are +// deterministic and prefixed (`org_`, `user_org__`) so +// the same WorkOS user in a different org gets a distinct scope row, and +// future workspace scopes can slot in between without colliding with org or +// user-org rows. // // OAuth tokens land at `ctx.scopes[0]` (the user-org scope) by default, so // a member's access/refresh tokens can't leak to other members via @@ -71,12 +71,12 @@ export const createScopedExecutor = ( const blobs = makePostgresBlobStore({ db }); const orgScope = new Scope({ - id: ScopeId.make(organizationId), + id: orgScopeId(organizationId), name: organizationName, createdAt: new Date(), }); const userOrgScope = new Scope({ - id: ScopeId.make(`user-org:${userId}:${organizationId}`), + id: userOrgScopeId(userId, organizationId), name: `Personal · ${organizationName}`, createdAt: new Date(), }); diff --git a/apps/cloud/src/services/secrets-api.node.test.ts b/apps/cloud/src/services/secrets-api.node.test.ts index 97214f41b..f18d6ef78 100644 --- a/apps/cloud/src/services/secrets-api.node.test.ts +++ b/apps/cloud/src/services/secrets-api.node.test.ts @@ -4,9 +4,14 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; -import { ScopeId, SecretId } from "@executor-js/sdk"; +import { SecretId } from "@executor-js/sdk"; -import { asOrg, fetchForOrg, TEST_BASE_URL } from "./__test-harness__/api-harness"; +import { + asOrg, + fetchForOrg, + orgScopeId, + TEST_BASE_URL, +} from "./__test-harness__/api-harness"; describe("secrets api (HTTP)", () => { it.effect("set → list → status round-trips a new secret without exposing plaintext", () => @@ -16,21 +21,21 @@ describe("secrets api (HTTP)", () => { const setRef = yield* asOrg(org, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { id: SecretId.make(id), name: "My API Token", value: "sk-test-abc" }, }), ); expect(setRef.id).toBe(id); - expect(setRef.scopeId).toBe(org); + expect(setRef.scopeId).toBe(orgScopeId(org)); const list = yield* asOrg(org, (client) => - client.secrets.list({ params: { scopeId: ScopeId.make(org) } }), + client.secrets.list({ params: { scopeId: orgScopeId(org) } }), ); expect(list.find((s) => s.id === id)?.name).toBe("My API Token"); const status = yield* asOrg(org, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(org), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(org), secretId: SecretId.make(id) }, }), ); expect(status.status).toBe("resolved"); @@ -44,7 +49,7 @@ describe("secrets api (HTTP)", () => { yield* asOrg(org, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { id: SecretId.make(id), name: "n", value: "v" }, }), ); @@ -63,14 +68,14 @@ describe("secrets api (HTTP)", () => { yield* asOrg(org, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { id: SecretId.make(id), name: "n", value: "v" }, }), ); const resolvedStatus = yield* asOrg(org, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(org), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(org), secretId: SecretId.make(id) }, }), ); expect(resolvedStatus.status).toBe("resolved"); @@ -78,7 +83,7 @@ describe("secrets api (HTTP)", () => { const missingStatus = yield* asOrg(org, (client) => client.secrets.status({ params: { - scopeId: ScopeId.make(org), + scopeId: orgScopeId(org), secretId: SecretId.make(`missing_${crypto.randomUUID().slice(0, 8)}`), }, }), @@ -95,23 +100,23 @@ describe("secrets api (HTTP)", () => { yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.secrets.set({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { id: SecretId.make(id), name: "n", value: "v" }, }); yield* client.secrets.remove({ - params: { scopeId: ScopeId.make(org), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(org), secretId: SecretId.make(id) }, }); }), ); const list = yield* asOrg(org, (client) => - client.secrets.list({ params: { scopeId: ScopeId.make(org) } }), + client.secrets.list({ params: { scopeId: orgScopeId(org) } }), ); expect(list.map((s) => s.id)).not.toContain(id); const afterStatus = yield* asOrg(org, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(org), secretId: SecretId.make(id) }, + params: { scopeId: orgScopeId(org), secretId: SecretId.make(id) }, }), ); expect(afterStatus.status).toBe("missing"); @@ -125,7 +130,7 @@ describe("secrets api (HTTP)", () => { const result = yield* asOrg(org, (client) => client.secrets - .remove({ params: { scopeId: ScopeId.make(org), secretId: SecretId.make(missing) } }) + .remove({ params: { scopeId: orgScopeId(org), secretId: SecretId.make(missing) } }) .pipe(Effect.result), ); expect(result._tag).toBe("Success"); @@ -140,10 +145,10 @@ describe("secrets api (HTTP)", () => { const first = yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.secrets.set({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { id: SecretId.make(id), name: "first", value: "first-value" }, }); - return yield* client.secrets.list({ params: { scopeId: ScopeId.make(org) } }); + return yield* client.secrets.list({ params: { scopeId: orgScopeId(org) } }); }), ); expect(first.find((s) => s.id === id)?.name).toBe("first"); @@ -151,10 +156,10 @@ describe("secrets api (HTTP)", () => { const second = yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.secrets.set({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { id: SecretId.make(id), name: "updated", value: "second-value" }, }); - return yield* client.secrets.list({ params: { scopeId: ScopeId.make(org) } }); + return yield* client.secrets.list({ params: { scopeId: orgScopeId(org) } }); }), ); expect(second.find((s) => s.id === id)?.name).toBe("updated"); diff --git a/apps/cloud/src/services/sources-api.node.test.ts b/apps/cloud/src/services/sources-api.node.test.ts index 0e0f0d0cc..2f0cb193b 100644 --- a/apps/cloud/src/services/sources-api.node.test.ts +++ b/apps/cloud/src/services/sources-api.node.test.ts @@ -12,11 +12,12 @@ import { resolve } from "node:path"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { ScopeId, SecretId } from "@executor-js/sdk"; +import { SecretId } from "@executor-js/sdk"; import { asOrg, asUser, + orgScopeId, testUserOrgScopeId, } from "./__test-harness__/api-harness"; @@ -306,7 +307,7 @@ describe("sources api (HTTP)", () => { yield* asOrg(org, (client) => Effect.gen(function* () { const result = yield* client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }); expect(result.namespace).toBe(namespace); @@ -315,7 +316,7 @@ describe("sources api (HTTP)", () => { ); const sources = yield* asOrg(org, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(org) } }), + client.sources.list({ params: { scopeId: orgScopeId(org) } }), ); expect(sources.map((s) => s.id)).toContain(namespace); }), @@ -328,13 +329,13 @@ describe("sources api (HTTP)", () => { yield* asOrg(org, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }), ); const fetched = yield* asOrg(org, (client) => - client.openapi.getSource({ params: { scopeId: ScopeId.make(org), namespace } }), + client.openapi.getSource({ params: { scopeId: orgScopeId(org), namespace } }), ); expect(fetched).not.toBeNull(); expect(fetched?.namespace).toBe(namespace); @@ -346,7 +347,7 @@ describe("sources api (HTTP)", () => { const org = `org_${crypto.randomUUID()}`; const preview = yield* asOrg(org, (client) => client.openapi.previewSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC }, }), ); @@ -372,7 +373,7 @@ describe("sources api (HTTP)", () => { ); const org = `org_${crypto.randomUUID()}`; const namespace = `ns_${crypto.randomUUID().replace(/-/g, "_")}`; - const scopeId = ScopeId.make(org); + const scopeId = orgScopeId(org); const addResult = yield* asOrg(org, (client) => client.openapi.addSpec({ @@ -432,7 +433,7 @@ describe("sources api (HTTP)", () => { Effect.gen(function* () { const org = `org_${crypto.randomUUID()}`; const namespace = `mcp_${crypto.randomUUID().replace(/-/g, "_")}`; - const scopeId = ScopeId.make(org); + const scopeId = orgScopeId(org); const addResult = yield* asOrg(org, (client) => client.mcp @@ -478,7 +479,7 @@ describe("sources api (HTTP)", () => { ); const org = `org_${crypto.randomUUID()}`; const namespace = `gql_${crypto.randomUUID().replace(/-/g, "_")}`; - const scopeId = ScopeId.make(org); + const scopeId = orgScopeId(org); const added = yield* asOrg(org, (client) => client.graphql.addSource({ @@ -541,7 +542,7 @@ describe("sources api (HTTP)", () => { ); const org = `org_${crypto.randomUUID()}`; const namespace = `mcp_${crypto.randomUUID().replace(/-/g, "_")}`; - const scopeId = ScopeId.make(org); + const scopeId = orgScopeId(org); const added = yield* asOrg(org, (client) => client.mcp.addSource({ @@ -609,17 +610,17 @@ describe("sources api (HTTP)", () => { yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }); yield* client.sources.remove({ - params: { scopeId: ScopeId.make(org), sourceId: namespace }, + params: { scopeId: orgScopeId(org), sourceId: namespace }, }); }), ); const after = yield* asOrg(org, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(org) } }), + client.sources.list({ params: { scopeId: orgScopeId(org) } }), ); expect(after.map((s) => s.id)).not.toContain(namespace); }), @@ -632,7 +633,7 @@ describe("sources api (HTTP)", () => { const result = yield* asOrg(org, (client) => client.sources - .remove({ params: { scopeId: ScopeId.make(org), sourceId: ghost } }) + .remove({ params: { scopeId: orgScopeId(org), sourceId: ghost } }) .pipe(Effect.result), ); expect(result._tag).toBe("Success"); @@ -648,7 +649,7 @@ describe("sources api (HTTP)", () => { const result = yield* asOrg(org, (client) => client.sources - .remove({ params: { scopeId: ScopeId.make(org), sourceId: "openapi" } }) + .remove({ params: { scopeId: orgScopeId(org), sourceId: "openapi" } }) .pipe(Effect.result), ); expect(result._tag).toBe("Failure"); @@ -663,18 +664,18 @@ describe("sources api (HTTP)", () => { yield* asOrg(org, (client) => Effect.gen(function* () { yield* client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace }, }); yield* client.openapi.updateSource({ - params: { scopeId: ScopeId.make(org), namespace }, + params: { scopeId: orgScopeId(org), namespace }, payload: { name: "Renamed API", baseUrl: "https://override.example.com" }, }); }), ); const fetched = yield* asOrg(org, (client) => - client.openapi.getSource({ params: { scopeId: ScopeId.make(org), namespace } }), + client.openapi.getSource({ params: { scopeId: orgScopeId(org), namespace } }), ); expect(fetched?.name).toBe("Renamed API"); expect(fetched?.config.baseUrl).toBe("https://override.example.com"); @@ -692,7 +693,7 @@ describe("sources api (HTTP)", () => { yield* asOrg(orgId, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(orgId) }, + params: { scopeId: orgScopeId(orgId) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace, @@ -710,7 +711,7 @@ describe("sources api (HTTP)", () => { yield* asUser(aliceId, orgId, (client) => Effect.gen(function* () { yield* client.secrets.set({ - params: { scopeId: ScopeId.make(aliceScope) }, + params: { scopeId: aliceScope }, payload: { id: SecretId.make("alice_pat"), name: "Alice PAT", @@ -718,11 +719,11 @@ describe("sources api (HTTP)", () => { }, }); const binding = yield* client.openapi.setSourceBinding({ - params: { scopeId: ScopeId.make(aliceScope) }, + params: { scopeId: aliceScope }, payload: { sourceId: namespace, - sourceScope: ScopeId.make(orgId), - scope: ScopeId.make(aliceScope), + sourceScope: orgScopeId(orgId), + scope: aliceScope, slot: "auth:personal-token", value: { kind: "secret", @@ -732,8 +733,8 @@ describe("sources api (HTTP)", () => { }); expect(binding).toMatchObject({ sourceId: namespace, - sourceScopeId: ScopeId.make(orgId), - scopeId: ScopeId.make(aliceScope), + sourceScopeId: orgScopeId(orgId), + scopeId: aliceScope, slot: "auth:personal-token", value: { kind: "secret", @@ -748,7 +749,7 @@ describe("sources api (HTTP)", () => { yield* asUser(bobId, orgId, (client) => Effect.gen(function* () { yield* client.secrets.set({ - params: { scopeId: ScopeId.make(bobScope) }, + params: { scopeId: bobScope }, payload: { id: SecretId.make("bob_pat"), name: "Bob PAT", @@ -756,11 +757,11 @@ describe("sources api (HTTP)", () => { }, }); yield* client.openapi.setSourceBinding({ - params: { scopeId: ScopeId.make(bobScope) }, + params: { scopeId: bobScope }, payload: { sourceId: namespace, - sourceScope: ScopeId.make(orgId), - scope: ScopeId.make(bobScope), + sourceScope: orgScopeId(orgId), + scope: bobScope, slot: "auth:personal-token", value: { kind: "secret", @@ -774,15 +775,15 @@ describe("sources api (HTTP)", () => { const aliceBindings = yield* asUser(aliceId, orgId, (client) => client.openapi.listSourceBindings({ params: { - scopeId: ScopeId.make(aliceScope), + scopeId: aliceScope, namespace, - sourceScopeId: ScopeId.make(orgId), + sourceScopeId: orgScopeId(orgId), }, }), ); expect(aliceBindings).toContainEqual( expect.objectContaining({ - scopeId: ScopeId.make(aliceScope), + scopeId: aliceScope, slot: "auth:personal-token", value: { kind: "secret", @@ -802,15 +803,15 @@ describe("sources api (HTTP)", () => { const bobBindings = yield* asUser(bobId, orgId, (client) => client.openapi.listSourceBindings({ params: { - scopeId: ScopeId.make(bobScope), + scopeId: bobScope, namespace, - sourceScopeId: ScopeId.make(orgId), + sourceScopeId: orgScopeId(orgId), }, }), ); expect(bobBindings).toContainEqual( expect.objectContaining({ - scopeId: ScopeId.make(bobScope), + scopeId: bobScope, slot: "auth:personal-token", value: { kind: "secret", @@ -828,10 +829,10 @@ describe("sources api (HTTP)", () => { ).toBe(false); const sources = yield* asOrg(orgId, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(orgId) } }), + client.sources.list({ params: { scopeId: orgScopeId(orgId) } }), ); expect(sources.find((source) => source.id === namespace)?.scopeId).toBe( - ScopeId.make(orgId), + orgScopeId(orgId), ); }), ); @@ -845,7 +846,7 @@ describe("sources api (HTTP)", () => { const result = yield* asOrg(org, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: CLOUDFLARE_SPEC, namespace }, }), ); @@ -853,7 +854,7 @@ describe("sources api (HTTP)", () => { expect(result.toolCount).toBeGreaterThan(1000); const sources = yield* asOrg(org, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(org) } }), + client.sources.list({ params: { scopeId: orgScopeId(org) } }), ); expect(sources.map((s) => s.id)).toContain(namespace); @@ -862,11 +863,11 @@ describe("sources api (HTTP)", () => { // fanning out to per-row deletes). yield* asOrg(org, (client) => client.sources.remove({ - params: { scopeId: ScopeId.make(org), sourceId: namespace }, + params: { scopeId: orgScopeId(org), sourceId: namespace }, }), ); const after = yield* asOrg(org, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(org) } }), + client.sources.list({ params: { scopeId: orgScopeId(org) } }), ); expect(after.map((s) => s.id)).not.toContain(namespace); }), diff --git a/apps/cloud/src/services/sources-refresh.node.test.ts b/apps/cloud/src/services/sources-refresh.node.test.ts index c6ee73334..5fb2d9e2d 100644 --- a/apps/cloud/src/services/sources-refresh.node.test.ts +++ b/apps/cloud/src/services/sources-refresh.node.test.ts @@ -9,9 +9,7 @@ import { Effect } from "effect"; import http from "node:http"; import { AddressInfo } from "node:net"; -import { ScopeId } from "@executor-js/sdk"; - -import { asOrg } from "./__test-harness__/api-harness"; +import { asOrg, orgScopeId } from "./__test-harness__/api-harness"; const specV1 = JSON.stringify({ openapi: "3.0.0", @@ -92,27 +90,27 @@ describe("sources.refresh (HTTP)", () => { yield* asOrg(org, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: `${server.baseUrl}/spec.json`, namespace }, }), ); const before = yield* asOrg(org, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(org) } }), + client.sources.list({ params: { scopeId: orgScopeId(org) } }), ); const beforeSource = before.find((s) => s.id === namespace); expect(beforeSource?.canRefresh).toBe(true); const fetchedBefore = yield* asOrg(org, (client) => client.openapi.getSource({ - params: { scopeId: ScopeId.make(org), namespace }, + params: { scopeId: orgScopeId(org), namespace }, }), ); expect(fetchedBefore?.config.sourceUrl).toBe(`${server.baseUrl}/spec.json`); const beforeTools = yield* asOrg(org, (client) => client.sources.tools({ - params: { scopeId: ScopeId.make(org), sourceId: namespace }, + params: { scopeId: orgScopeId(org), sourceId: namespace }, }), ); expect(beforeTools.length).toBe(1); @@ -125,7 +123,7 @@ describe("sources.refresh (HTTP)", () => { const refreshResult = yield* asOrg(org, (client) => client.sources.refresh({ - params: { scopeId: ScopeId.make(org), sourceId: namespace }, + params: { scopeId: orgScopeId(org), sourceId: namespace }, }), ); expect(refreshResult.refreshed).toBe(true); @@ -133,7 +131,7 @@ describe("sources.refresh (HTTP)", () => { const afterTools = yield* asOrg(org, (client) => client.sources.tools({ - params: { scopeId: ScopeId.make(org), sourceId: namespace }, + params: { scopeId: orgScopeId(org), sourceId: namespace }, }), ); expect(afterTools.length).toBe(2); @@ -152,13 +150,13 @@ describe("sources.refresh (HTTP)", () => { yield* asOrg(org, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(org) }, + params: { scopeId: orgScopeId(org) }, payload: { spec: specV1, namespace }, }), ); const sources = yield* asOrg(org, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(org) } }), + client.sources.list({ params: { scopeId: orgScopeId(org) } }), ); const row = sources.find((s) => s.id === namespace); expect(row?.canRefresh).toBe(false); @@ -168,7 +166,7 @@ describe("sources.refresh (HTTP)", () => { // server should not 500 if a caller slips through. const result = yield* asOrg(org, (client) => client.sources.refresh({ - params: { scopeId: ScopeId.make(org), sourceId: namespace }, + params: { scopeId: orgScopeId(org), sourceId: namespace }, }), ); expect(result.refreshed).toBe(true); diff --git a/apps/cloud/src/services/tenant-isolation.node.test.ts b/apps/cloud/src/services/tenant-isolation.node.test.ts index db89cdf87..e76273c96 100644 --- a/apps/cloud/src/services/tenant-isolation.node.test.ts +++ b/apps/cloud/src/services/tenant-isolation.node.test.ts @@ -5,9 +5,9 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect } from "effect"; -import { ScopeId, SecretId } from "@executor-js/sdk"; +import { SecretId } from "@executor-js/sdk"; -import { asOrg } from "./__test-harness__/api-harness"; +import { asOrg, orgScopeId } from "./__test-harness__/api-harness"; const MINIMAL_OPENAPI_SPEC = JSON.stringify({ openapi: "3.0.0", @@ -31,13 +31,13 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace: namespaceA }, }), ); const orgBSources = yield* asOrg(orgB, (client) => - client.sources.list({ params: { scopeId: ScopeId.make(orgB) } }), + client.sources.list({ params: { scopeId: orgScopeId(orgB) } }), ); expect(orgBSources.map((s) => s.id)).not.toContain(namespaceA); }), @@ -51,13 +51,13 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace: namespaceA }, }), ); const orgBTools = yield* asOrg(orgB, (client) => - client.tools.list({ params: { scopeId: ScopeId.make(orgB) } }), + client.tools.list({ params: { scopeId: orgScopeId(orgB) } }), ); expect(orgBTools.map((t) => t.sourceId)).not.toContain(namespaceA); }), @@ -71,14 +71,14 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace: namespaceA }, }), ); const result = yield* asOrg(orgB, (client) => client.openapi - .getSource({ params: { scopeId: ScopeId.make(orgB), namespace: namespaceA } }) + .getSource({ params: { scopeId: orgScopeId(orgB), namespace: namespaceA } }) .pipe(Effect.result), ); @@ -96,13 +96,13 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { id: SecretId.make(secretIdA), name: "org-a only", value: "super-secret-a" }, }), ); const orgBSecrets = yield* asOrg(orgB, (client) => - client.secrets.list({ params: { scopeId: ScopeId.make(orgB) } }), + client.secrets.list({ params: { scopeId: orgScopeId(orgB) } }), ); expect(orgBSecrets.map((s) => s.id)).not.toContain(secretIdA); }), @@ -116,14 +116,14 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { id: SecretId.make(secretIdA), name: "org-a only", value: "super-secret-a" }, }), ); const result = yield* asOrg(orgB, (client) => client.secrets - .status({ params: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(secretIdA) } }) + .status({ params: { scopeId: orgScopeId(orgB), secretId: SecretId.make(secretIdA) } }) .pipe(Effect.result), ); @@ -141,18 +141,18 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.secrets.set({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { id: SecretId.make(secretIdA), name: "org-a only", value: "super-secret-a" }, }), ); const status = yield* asOrg(orgB, (client) => client.secrets.status({ - params: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(secretIdA) }, + params: { scopeId: orgScopeId(orgB), secretId: SecretId.make(secretIdA) }, }), ); const list = yield* asOrg(orgB, (client) => - client.secrets.list({ params: { scopeId: ScopeId.make(orgB) } }), + client.secrets.list({ params: { scopeId: orgScopeId(orgB) } }), ); expect(status.status).toBe("missing"); @@ -168,7 +168,7 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(orgA) }, + params: { scopeId: orgScopeId(orgA) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace, @@ -179,7 +179,7 @@ describe("tenant isolation (HTTP)", () => { ); yield* asOrg(orgB, (client) => client.openapi.addSpec({ - params: { scopeId: ScopeId.make(orgB) }, + params: { scopeId: orgScopeId(orgB) }, payload: { spec: MINIMAL_OPENAPI_SPEC, namespace, @@ -191,7 +191,7 @@ describe("tenant isolation (HTTP)", () => { yield* asOrg(orgA, (client) => client.openapi.updateSource({ - params: { scopeId: ScopeId.make(orgA), namespace }, + params: { scopeId: orgScopeId(orgA), namespace }, payload: { name: "Org A Updated API", baseUrl: "https://org-a-updated.example.com", @@ -200,10 +200,10 @@ describe("tenant isolation (HTTP)", () => { ); const orgASource = yield* asOrg(orgA, (client) => - client.openapi.getSource({ params: { scopeId: ScopeId.make(orgA), namespace } }), + client.openapi.getSource({ params: { scopeId: orgScopeId(orgA), namespace } }), ); const orgBSource = yield* asOrg(orgB, (client) => - client.openapi.getSource({ params: { scopeId: ScopeId.make(orgB), namespace } }), + client.openapi.getSource({ params: { scopeId: orgScopeId(orgB), namespace } }), ); expect(orgASource?.name).toBe("Org A Updated API"); expect(orgASource?.config.baseUrl).toBe("https://org-a-updated.example.com");