Skip to content

Commit 6f10936

Browse files
hreitenColeMurraykadams54merlin-mk
authored
chore: sync upstream main (#35)
* Allow Slack sessions to use OpenCode titles (ColeMurray#685) ## Summary - Stop pre-filling Slack-created session titles from the Slack message or repo fallback - Leave Slack session titles unset so OpenCode-generated title events can populate them - Add a regression assertion for the Slack session creation payload ## Validation - npm run build -w @open-inspect/shared - npm test -w @open-inspect/slack-bot - npm run typecheck -w @open-inspect/slack-bot - npm run lint -w @open-inspect/slack-bot <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Removed title field from session creation requests to the control plane. * **Tests** * Updated tests to verify title field is not included in session creation requests. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/ColeMurray/background-agents/pull/685?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> * Add provider identity resolution API (ColeMurray#686) ## Summary - Add a shared canonical user ID validator for 32-character lowercase hex D1 user IDs. - Add an internal HMAC-authenticated `PUT /provider-identities/:provider/:providerUserId` control-plane route that upserts provider identity metadata through `UserStore.resolveOrCreateUser` and returns the canonical `userId`. - Add browser-facing `GET /api/me`, deriving GitHub identity from the server-side NextAuth session and returning only the canonical user ID. ## Validation - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/shared -- user-id` - `npm run typecheck -w @open-inspect/shared` - `npm run lint -w @open-inspect/shared -- --quiet` - `npm test -w @open-inspect/control-plane -- routes/provider-identities.test.ts router.provider-identities.test.ts` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane -- --quiet` - `npm test -w @open-inspect/web -- app/api/me/route.test.ts` - `npm run typecheck -w @open-inspect/web` - `npm run lint -w @open-inspect/web -- --quiet` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * New API to return the authenticated user's GitHub-derived identity and canonical user ID. * New endpoints to upsert GitHub provider identities and return a canonical user ID. * Shared validator ensuring canonical 32-character lowercase hex user IDs. * **Bug Fixes** * Improved input validation and explicit error responses for invalid provider-user IDs and malformed requests. * Better error handling when identity resolution returns unexpected values. * **Tests** * Added integration and unit tests covering success, validation failures, and error paths. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/ColeMurray/background-agents/pull/686?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> * Filter sessions by creator (ColeMurray#688) ## Summary - Add `createdBy` session-list filtering for canonical user IDs in the control plane and web BFF - Add a sidebar All/Mine filter that resolves the current canonical user via `/api/me` - Revalidate all session-list SWR cache variants and add a D1 index for creator-filtered recency queries ## Validation - `npm test -w @open-inspect/control-plane -- src/db/session-index.test.ts` - `npm run test:integration -w @open-inspect/control-plane -- test/integration/auth.test.ts` - `npm test -w @open-inspect/web -- src/lib/session-list.test.ts src/lib/control-plane-query.test.ts src/components/session-sidebar.test.tsx` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane -- --quiet` - `npm run typecheck -w @open-inspect/web` - `npm run lint -w @open-inspect/web -- --quiet` - `npm run build -w @open-inspect/shared` - `npm run build -w @open-inspect/control-plane` - `npm run build -w @open-inspect/web` - `npx prettier --check ...changed TS/TSX files` - `git diff --check` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Sidebar ownership filter (All vs Mine); session list supports filtering by creator (createdBy) and scope=mine resolves the current user. * **Performance** * New DB index to speed creator-filtered session queries. * **Improvements** * Unified session-list cache keys and more reliable cache invalidation for rename, archive/unarchive, and pagination. * **Bug Fixes** * Invalid createdBy or scope now return proper 400 errors. * **Tests** * Added coverage for createdBy/scope handling, cache-key behavior, and related flows. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/ColeMurray/background-agents/pull/688?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> * fix(modal): support non-main Modal environments (ColeMurray#687) ## Summary - Takes over ColeMurray#629 on a new branch because the original PR head is company-owned. - Adds `modal_environment` through the Modal Terraform module so secrets and deploy commands run in the selected Modal environment. - Adds `modal_environment_web_suffix` / `MODAL_ENVIRONMENT_WEB_SUFFIX` for Modal endpoint URL hosts, keeping it separate from the CLI environment name. - Keeps `MODAL_WORKSPACE` as the raw workspace, passes `MODAL_ENVIRONMENT` for dashboard links, and uses the web suffix for Modal API endpoint slugs. - Documents the new Modal environment settings and wires them into Terraform GitHub Actions. ## Addresses Review Feedback - Owner thread on `terraform/environments/production/modal.tf`: Modal CLI now receives `MODAL_ENVIRONMENT` for both secret creation and deploys. - Owner thread on `packages/control-plane/src/sandbox/client.ts`: endpoint hosts now use explicit Modal web suffix instead of deriving from environment name. - CodeRabbit validation note on `modal_environment`: rejects empty values, colons, slashes, and backslashes for Modal deployments, including at the module boundary. - CodeRabbit deploy script note: validates all required env vars before first use under `set -u`. ## Validation - `npm run build -w @open-inspect/shared` - `npm test -w @open-inspect/control-plane -- --run src/sandbox/client.test.ts` - `npm run typecheck -w @open-inspect/control-plane` - `npm run lint -w @open-inspect/control-plane` - `terraform fmt -recursive -check terraform` - `terraform -chdir=terraform/environments/production init -backend=false` - `terraform -chdir=terraform/environments/production validate` - `bash -n terraform/modules/modal-app/scripts/deploy.sh` - `bash -n terraform/modules/modal-app/scripts/create-secrets.sh` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Add selectable Modal deployment environments and pass environment to Modal clients and deploy tooling * Configurable environment-to-endpoint web-suffix for Modal URLs and health checks * **Documentation** * Updated setup, CI, and Terraform docs to document workspace, environment, and web-suffix and new secrets/vars * **Tests** * Added tests for workspace-slug generation and environment-specific client health endpoints <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/ColeMurray/background-agents/pull/687?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Kyle Adams <kadams54@users.noreply.github.com> * fix(slack-bot): cap App Home repo-override list under Slack's block limit (ColeMurray#689) ## Problem The App Home tab renders **one block per repo-specific branch override** with no upper bound (`slack-bot/src/index.ts`). Slack's `views.publish` rejects any view over **100 blocks**, so once a user configures ~85+ repo overrides the entire Home tab fails to publish — including the UI needed to manage those overrides, leaving the user stuck. ## Fix - Cap rendered overrides at **50** (`MAX_RENDERED_REPO_OVERRIDES`), well under the 100-block ceiling (~15 base blocks). - Surface the remainder as a `…and N more` summary instead of dropping it silently. - Hidden overrides stay **fully manageable** via the existing "Search repository" selector → modal, which clears any repo's override on empty submit — so the cap strands nothing. ## Test Adds a regression test that drives the real `app_home_opened` → `views.publish` path with 60 overrides and asserts the published view stays ≤ 100 blocks, renders exactly 50 rows, and includes the "10 more overrides" note. Full slack-bot suite (87 tests) + typecheck pass. Found while syncing this change into a downstream fork. Happy to adjust the cap value or copy. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of large numbers of repository overrides in the Slack App Home interface. The interface now displays up to 50 overrides with a summary note indicating additional hidden overrides, ensuring functionality within Slack's technical constraints. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/ColeMurray/background-agents/pull/689?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> * Refactor Slack App Home module (ColeMurray#690) ## Summary - move Slack App Home publishing, interaction handling, view construction, modals, metadata, model lookup, and local Slack view types into focused `app-home/` modules - extract user preference persistence/resolution into `user-preferences.ts` - keep the `/interactions` route delegating App Home-specific payloads while leaving generic Slack interaction handling in `index.ts` - move App Home view unit coverage into `app-home.test.ts` ## Validation - `npm run typecheck -w @open-inspect/slack-bot` - `npm run lint -w @open-inspect/slack-bot` - `npx prettier --check packages/slack-bot/src/app-home packages/slack-bot/src/app-home.test.ts packages/slack-bot/src/user-preferences.ts` - `git diff --check` - `npm test -w @open-inspect/slack-bot` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * App Home settings: model selection, reasoning-effort choice, global and per-repo branch overrides with modals. * Repo search suggestions and truncated option labels to fit Slack limits; long override lists show a “more” summary. * **Refactor** * Centralized App Home interaction routing and unified preference resolution. * App Home publishing made asynchronous and robust. * **Tests** * Expanded coverage for App Home view, suggestions, truncation, and preference updates. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cole Murray <colemurray.cs@gmail.com> Co-authored-by: Kyle Adams <kadams54@users.noreply.github.com> Co-authored-by: Merlin <40275364+merlin-mk@users.noreply.github.com>
1 parent f7bb22c commit 6f10936

60 files changed

Lines changed: 3035 additions & 1043 deletions

Some content is hidden

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

.github/workflows/terraform.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ jobs:
189189
TF_VAR_modal_token_id: ${{ secrets.MODAL_TOKEN_ID }}
190190
TF_VAR_modal_token_secret: ${{ secrets.MODAL_TOKEN_SECRET }}
191191
TF_VAR_modal_workspace: ${{ secrets.MODAL_WORKSPACE }}
192+
TF_VAR_modal_environment: "${{ secrets.MODAL_ENVIRONMENT || 'main' }}"
193+
TF_VAR_modal_environment_web_suffix: ${{ secrets.MODAL_ENVIRONMENT_WEB_SUFFIX }}
192194
TF_VAR_github_client_id: ${{ secrets.GH_OAUTH_CLIENT_ID }}
193195
TF_VAR_github_client_secret: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
194196
TF_VAR_github_app_id: ${{ secrets.GH_APP_ID }}
@@ -323,6 +325,8 @@ jobs:
323325
TF_VAR_modal_token_id: ${{ secrets.MODAL_TOKEN_ID }}
324326
TF_VAR_modal_token_secret: ${{ secrets.MODAL_TOKEN_SECRET }}
325327
TF_VAR_modal_workspace: ${{ secrets.MODAL_WORKSPACE }}
328+
TF_VAR_modal_environment: "${{ secrets.MODAL_ENVIRONMENT || 'main' }}"
329+
TF_VAR_modal_environment_web_suffix: ${{ secrets.MODAL_ENVIRONMENT_WEB_SUFFIX }}
326330
TF_VAR_github_client_id: ${{ secrets.GH_OAUTH_CLIENT_ID }}
327331
TF_VAR_github_client_secret: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
328332
TF_VAR_github_app_id: ${{ secrets.GH_APP_ID }}

docs/GETTING_STARTED.md

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ Create an R2 API Token:
150150
1. Go to [Modal Settings](https://modal.com/settings)
151151
2. **Create a new API token**: Settings -> API Tokens -> New Token
152152
3. Note the **Token ID** and **Token Secret**
153-
4. Note your **Workspace name** (visible in your Modal dashboard URL)
153+
4. Note your **Workspace** and **Environment name** (visible in your Modal dashboard URL,
154+
https://modal.com/apps/<modal_workspace>/<modal_environment>)
155+
5. Note the environment's **Web suffix** from Modal's environment settings. Use the normalized
156+
lowercase suffix made of letters, digits, and dashes. Leave it empty for the environment whose
157+
endpoints use `https://<workspace>--...modal.run`.
154158

155159
### Daytona
156160

@@ -361,6 +365,8 @@ vercel_team_id = "team_xxxxx" # Your Vercel ID (even personal
361365
modal_token_id = "your-modal-token-id"
362366
modal_token_secret = "your-modal-token-secret"
363367
modal_workspace = "your-modal-workspace"
368+
modal_environment = "your-modal-environment"
369+
modal_environment_web_suffix = "your-modal-web-suffix" # Lowercase letters, digits, dashes; empty for https://workspace--... endpoints
364370
365371
# Daytona (only required when sandbox_provider = "daytona")
366372
# daytona_api_url = "https://app.daytona.io/api"
@@ -634,8 +640,11 @@ Or manually:
634640
# 1. Control Plane health check (replace {deployment_name} and YOUR-SUBDOMAIN)
635641
curl https://open-inspect-control-plane-{deployment_name}.YOUR-SUBDOMAIN.workers.dev/health
636642
637-
# 2. Modal health check (replace YOUR-WORKSPACE)
638-
curl https://YOUR-WORKSPACE--open-inspect-api-health.modal.run
643+
# 2. Modal health check
644+
# Prefer the exact URL from terraform output verification_commands.
645+
# Manual form: https://<workspace>[-<modal_environment_web_suffix>]--open-inspect-api-health.modal.run
646+
MODAL_WORKSPACE_SLUG="YOUR-WORKSPACE" # or "YOUR-WORKSPACE-YOUR-MODAL-WEB-SUFFIX"
647+
curl https://${MODAL_WORKSPACE_SLUG}--open-inspect-api-health.modal.run
639648
640649
# 3. Web app (should return 200)
641650
# Vercel:
@@ -659,46 +668,48 @@ Enable automatic deployments when you push to main by adding GitHub Secrets.
659668
660669
Go to your fork's Settings → Secrets and variables → Actions, and add:
661670
662-
| Secret Name | Value |
663-
| ----------------------------- | ----------------------------------------------------------------------------- |
664-
| `CLOUDFLARE_API_TOKEN` | Your Cloudflare API token |
665-
| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |
666-
| `CLOUDFLARE_WORKER_SUBDOMAIN` | Your workers.dev subdomain |
667-
| `DEPLOYMENT_NAME` | Your deployment name |
668-
| `R2_ACCESS_KEY_ID` | R2 access key ID |
669-
| `R2_SECRET_ACCESS_KEY` | R2 secret access key |
670-
| `WEB_PLATFORM` | `vercel` or `cloudflare` |
671-
| `VERCEL_API_TOKEN` | Vercel API token _(only if `web_platform = "vercel"`)_ |
672-
| `VERCEL_TEAM_ID` | Vercel team/account ID _(only if `web_platform = "vercel"`)_ |
673-
| `VERCEL_PROJECT_ID` | Vercel project ID _(only if `web_platform = "vercel"`)_ |
674-
| `NEXTAUTH_URL` | Your web app URL |
675-
| `MODAL_TOKEN_ID` | Modal token ID |
676-
| `MODAL_TOKEN_SECRET` | Modal token secret |
677-
| `MODAL_WORKSPACE` | Modal workspace name |
678-
| `GH_OAUTH_CLIENT_ID` | GitHub App OAuth client ID |
679-
| `GH_OAUTH_CLIENT_SECRET` | GitHub App OAuth client secret |
680-
| `GH_APP_ID` | GitHub App ID |
681-
| `GH_APP_PRIVATE_KEY` | GitHub App private key (PKCS#8 format) |
682-
| `GH_APP_INSTALLATION_ID` | GitHub App installation ID |
683-
| `ENABLE_SLACK_BOT` | `true` to deploy Slack bot, `false` to skip (default: `true`) |
684-
| `SLACK_BOT_TOKEN` | Slack bot token (required if enabled) |
685-
| `SLACK_SIGNING_SECRET` | Slack signing secret (required if enabled) |
686-
| `ANTHROPIC_API_KEY` | Optional Anthropic API key for metered fallback and bot classifiers |
687-
| `ANTHROPIC_OAUTH_CLIENT_ID` | Optional Claude subscription OAuth public client ID override |
688-
| `ANTHROPIC_OAUTH_TOKEN_URL` | Optional Claude subscription OAuth token endpoint override |
689-
| `TOKEN_ENCRYPTION_KEY` | Generated encryption key (OAuth tokens) |
690-
| `REPO_SECRETS_ENCRYPTION_KEY` | Generated encryption key (repo secrets) |
691-
| `INTERNAL_CALLBACK_SECRET` | Generated callback secret |
692-
| `MODAL_API_SECRET` | Generated Modal API secret |
693-
| `NEXTAUTH_SECRET` | Generated NextAuth secret |
694-
| `ALLOWED_USERS` | Comma-separated GitHub usernames (or empty for all users) |
695-
| `ALLOWED_EMAIL_DOMAINS` | Comma-separated email domains (or empty for all domains) |
696-
| `ENABLE_GITHUB_BOT` | `true` to deploy GitHub bot worker (or empty to skip) |
697-
| `GH_WEBHOOK_SECRET` | GitHub webhook secret (required if GitHub bot enabled) |
698-
| `GH_BOT_USERNAME` | GitHub App bot username, e.g., `my-app[bot]` (required if GitHub bot enabled) |
699-
| `APP_NAME` | Optional display name for whitelabeling (default: `Open-Inspect`) |
700-
| `APP_SHORT_NAME` | Optional short label for sidebar header (default: `Inspect`) |
701-
| `APP_ICON_URL` | Optional URL to a custom logo/favicon (default: built-in icon) |
671+
| Secret Name | Value |
672+
| ------------------------------ | ------------------------------------------------------------------------------------------- |
673+
| `CLOUDFLARE_API_TOKEN` | Your Cloudflare API token |
674+
| `CLOUDFLARE_ACCOUNT_ID` | Your Cloudflare account ID |
675+
| `CLOUDFLARE_WORKER_SUBDOMAIN` | Your workers.dev subdomain |
676+
| `DEPLOYMENT_NAME` | Your deployment name |
677+
| `R2_ACCESS_KEY_ID` | R2 access key ID |
678+
| `R2_SECRET_ACCESS_KEY` | R2 secret access key |
679+
| `WEB_PLATFORM` | `vercel` or `cloudflare` |
680+
| `VERCEL_API_TOKEN` | Vercel API token _(only if `web_platform = "vercel"`)_ |
681+
| `VERCEL_TEAM_ID` | Vercel team/account ID _(only if `web_platform = "vercel"`)_ |
682+
| `VERCEL_PROJECT_ID` | Vercel project ID _(only if `web_platform = "vercel"`)_ |
683+
| `NEXTAUTH_URL` | Your web app URL |
684+
| `MODAL_TOKEN_ID` | Modal token ID |
685+
| `MODAL_TOKEN_SECRET` | Modal token secret |
686+
| `MODAL_WORKSPACE` | Modal workspace name |
687+
| `MODAL_ENVIRONMENT` | Modal environment name (defaults to `main`) |
688+
| `MODAL_ENVIRONMENT_WEB_SUFFIX` | Modal environment web suffix for endpoint URLs; lowercase letters, digits, dashes, or empty |
689+
| `GH_OAUTH_CLIENT_ID` | GitHub App OAuth client ID |
690+
| `GH_OAUTH_CLIENT_SECRET` | GitHub App OAuth client secret |
691+
| `GH_APP_ID` | GitHub App ID |
692+
| `GH_APP_PRIVATE_KEY` | GitHub App private key (PKCS#8 format) |
693+
| `GH_APP_INSTALLATION_ID` | GitHub App installation ID |
694+
| `ENABLE_SLACK_BOT` | `true` to deploy Slack bot, `false` to skip (default: `true`) |
695+
| `SLACK_BOT_TOKEN` | Slack bot token (required if enabled) |
696+
| `SLACK_SIGNING_SECRET` | Slack signing secret (required if enabled) |
697+
| `ANTHROPIC_API_KEY` | Optional Anthropic API key for metered fallback and bot classifiers |
698+
| `ANTHROPIC_OAUTH_CLIENT_ID` | Optional Claude subscription OAuth public client ID override |
699+
| `ANTHROPIC_OAUTH_TOKEN_URL` | Optional Claude subscription OAuth token endpoint override |
700+
| `TOKEN_ENCRYPTION_KEY` | Generated encryption key (OAuth tokens) |
701+
| `REPO_SECRETS_ENCRYPTION_KEY` | Generated encryption key (repo secrets) |
702+
| `INTERNAL_CALLBACK_SECRET` | Generated callback secret |
703+
| `MODAL_API_SECRET` | Generated Modal API secret |
704+
| `NEXTAUTH_SECRET` | Generated NextAuth secret |
705+
| `ALLOWED_USERS` | Comma-separated GitHub usernames (or empty for all users) |
706+
| `ALLOWED_EMAIL_DOMAINS` | Comma-separated email domains (or empty for all domains) |
707+
| `ENABLE_GITHUB_BOT` | `true` to deploy GitHub bot worker (or empty to skip) |
708+
| `GH_WEBHOOK_SECRET` | GitHub webhook secret (required if GitHub bot enabled) |
709+
| `GH_BOT_USERNAME` | GitHub App bot username, e.g., `my-app[bot]` (required if GitHub bot enabled) |
710+
| `APP_NAME` | Optional display name for whitelabeling (default: `Open-Inspect`) |
711+
| `APP_SHORT_NAME` | Optional short label for sidebar header (default: `Inspect`) |
712+
| `APP_ICON_URL` | Optional URL to a custom logo/favicon (default: built-in icon) |
702713
703714
**Bulk upload secrets with `gh` CLI:**
704715

packages/control-plane/src/db/session-index.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,14 @@ class FakeD1Database {
307307
const nameVal = args[argIdx++] as string;
308308
rows = rows.filter((r) => r.repo_name === nameVal);
309309
}
310+
311+
const userIdMatch = conditions.match(/user_id IN \(([^)]+)\)/);
312+
if (userIdMatch) {
313+
const userIdCount = userIdMatch[1].split(",").length;
314+
const userIds = new Set(args.slice(argIdx, argIdx + userIdCount) as string[]);
315+
argIdx += userIdCount;
316+
rows = rows.filter((r) => r.user_id !== null && userIds.has(r.user_id));
317+
}
310318
}
311319

312320
return rows;
@@ -484,6 +492,30 @@ describe("SessionIndexStore", () => {
484492
expect(result.total).toBe(2);
485493
});
486494

495+
it("filters by creator user ids", async () => {
496+
await store.create(makeSession({ id: "alice-old", userId: "alice", updatedAt: 1000 }));
497+
await store.create(makeSession({ id: "bob", userId: "bob", updatedAt: 3000 }));
498+
await store.create(makeSession({ id: "alice-new", userId: "alice", updatedAt: 4000 }));
499+
await store.create(makeSession({ id: "historical", userId: null, updatedAt: 5000 }));
500+
501+
const result = await store.list({ createdByUserIds: ["alice"] });
502+
503+
expect(result.sessions.map((s) => s.id)).toEqual(["alice-new", "alice-old"]);
504+
expect(result.total).toBe(2);
505+
expect(result.hasMore).toBe(false);
506+
});
507+
508+
it("supports multiple creator user ids", async () => {
509+
await store.create(makeSession({ id: "alice", userId: "alice", updatedAt: 1000 }));
510+
await store.create(makeSession({ id: "bob", userId: "bob", updatedAt: 3000 }));
511+
await store.create(makeSession({ id: "carol", userId: "carol", updatedAt: 4000 }));
512+
513+
const result = await store.list({ createdByUserIds: ["alice", "bob"] });
514+
515+
expect(result.sessions.map((s) => s.id)).toEqual(["bob", "alice"]);
516+
expect(result.total).toBe(2);
517+
});
518+
487519
it("supports pagination with limit and offset", async () => {
488520
for (let i = 0; i < 5; i++) {
489521
await store.create(makeSession({ id: `s${i}`, updatedAt: i * 1000 }));

packages/control-plane/src/db/session-index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface ListSessionsOptions {
5454
excludeStatus?: SessionStatus;
5555
repoOwner?: string;
5656
repoName?: string;
57+
createdByUserIds?: readonly string[];
5758
limit?: number;
5859
offset?: number;
5960
}
@@ -131,7 +132,15 @@ export class SessionIndexStore {
131132
}
132133

133134
async list(options: ListSessionsOptions = {}): Promise<ListSessionsResult> {
134-
const { status, excludeStatus, repoOwner, repoName, limit = 50, offset = 0 } = options;
135+
const {
136+
status,
137+
excludeStatus,
138+
repoOwner,
139+
repoName,
140+
createdByUserIds,
141+
limit = 50,
142+
offset = 0,
143+
} = options;
135144

136145
const conditions: string[] = [];
137146
const params: unknown[] = [];
@@ -156,6 +165,11 @@ export class SessionIndexStore {
156165
params.push(repoName.toLowerCase());
157166
}
158167

168+
if (createdByUserIds?.length) {
169+
conditions.push(`user_id IN (${createdByUserIds.map(() => "?").join(", ")})`);
170+
params.push(...createdByUserIds);
171+
}
172+
159173
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
160174

161175
// Get total count
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { generateInternalToken } from "./auth/internal";
3+
import { handleRequest } from "./router";
4+
5+
const mockUserStore = {
6+
resolveOrCreateUser: vi.fn(),
7+
};
8+
9+
vi.mock("./db/user-store", () => ({
10+
UserStore: vi.fn().mockImplementation(() => mockUserStore),
11+
}));
12+
13+
describe("provider identity router integration", () => {
14+
beforeEach(() => {
15+
vi.clearAllMocks();
16+
mockUserStore.resolveOrCreateUser.mockResolvedValue({
17+
id: "0123456789abcdef0123456789abcdef",
18+
displayName: "Ada",
19+
email: "ada@example.com",
20+
isNew: false,
21+
});
22+
});
23+
24+
it("serves provider identity upserts even when the SCM provider is not github", async () => {
25+
const env = {
26+
INTERNAL_CALLBACK_SECRET: "test-secret",
27+
SCM_PROVIDER: "gitlab",
28+
DB: {
29+
prepare: vi.fn(),
30+
batch: vi.fn(),
31+
exec: vi.fn(),
32+
dump: vi.fn(),
33+
},
34+
};
35+
36+
const token = await generateInternalToken(env.INTERNAL_CALLBACK_SECRET);
37+
const response = await handleRequest(
38+
new Request("https://test.local/provider-identities/github/12345", {
39+
method: "PUT",
40+
headers: {
41+
"Content-Type": "application/json",
42+
Authorization: `Bearer ${token}`,
43+
},
44+
body: JSON.stringify({
45+
providerLogin: "ada",
46+
}),
47+
}),
48+
env as never
49+
);
50+
51+
expect(response.status).toBe(200);
52+
await expect(response.json()).resolves.toEqual({
53+
userId: "0123456789abcdef0123456789abcdef",
54+
});
55+
});
56+
});

0 commit comments

Comments
 (0)