From a8cb71f2dede3a475b4ed72a4de8e544d839d276 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 18:51:25 +0000
Subject: [PATCH 1/7] Use username-scoped public URLs
Co-authored-by: Kent C. Dodds
---
.../contributing/architecture/data-storage.md | 4 +-
.../architecture/remote-connectors.md | 4 +-
.../architecture/request-lifecycle.md | 4 +-
docs/contributing/package-invocation-api.md | 12 +--
docs/contributing/security.md | 4 +-
e2e/remote-connectors.spec.ts | 4 +-
packages/shared/src/chat.ts | 1 +
packages/shared/src/public-urls.ts | 26 ++++++
packages/shared/src/remote-connectors.ts | 8 +-
.../routes/account-remote-connectors.tsx | 12 +--
packages/worker/src/app/authenticated-user.ts | 1 +
.../account-remote-connectors.node.test.ts | 10 ++-
.../app/handlers/account-remote-connectors.ts | 7 +-
.../src/app/handlers/package-app.node.test.ts | 81 +++++--------------
.../worker/src/app/handlers/package-app.ts | 33 +++++---
packages/worker/src/app/router.ts | 2 +-
packages/worker/src/app/user-lookup.ts | 31 +++++++
packages/worker/src/index.ts | 19 ++++-
.../mcp/tools/open-generated-ui.node.test.ts | 4 +-
.../worker/src/mcp/tools/open-generated-ui.ts | 11 ++-
.../src/mcp/tools/search-format.node.test.ts | 8 +-
.../worker/src/mcp/tools/search-format.ts | 19 ++++-
packages/worker/src/mcp/tools/search.ts | 17 +++-
packages/worker/src/oauth-handlers.ts | 14 +++-
.../worker/src/package-invocations/http.ts | 46 +++++++----
.../package-invocations/http.workers.test.ts | 34 +++++---
.../worker/src/package-runtime/package-app.ts | 2 +
.../connector-session-key.node.test.ts | 22 ++---
.../remote-connector/connector-session-key.ts | 28 +++----
.../remote-connectors-shared.node.test.ts | 6 +-
.../worker/src/remote-connector/session.ts | 2 +-
.../public-route-hardening.workers.test.ts | 18 +++--
packages/worker/wrangler.jsonc | 24 +++++-
tools/ci/resource-utils.node.test.ts | 8 +-
34 files changed, 337 insertions(+), 189 deletions(-)
create mode 100644 packages/shared/src/public-urls.ts
create mode 100644 packages/worker/src/app/user-lookup.ts
diff --git a/docs/contributing/architecture/data-storage.md b/docs/contributing/architecture/data-storage.md
index 1a4e1e7163..c944ec39ff 100644
--- a/docs/contributing/architecture/data-storage.md
+++ b/docs/contributing/architecture/data-storage.md
@@ -93,8 +93,8 @@ that two different users always resolve to two different object ids:
`packages/worker/src/package-runtime/`.
- `RemoteConnectorSession` —
`userScopedConnectorSessionKey(userId, kind, instanceId)`. Connectors must
- connect through the new user-scoped ingress URL
- `/connectors/u/{userId}/{kind}/{instanceId}`. The DO carries the ingress user
+ connect through the username-scoped ingress URL
+ `/@{username}/connectors/{kind}/{instanceId}`. The DO carries the ingress user
id forward via headers + websocket attachment and verifies the shared secret
against that user's row only.
- `AgentTurnRunner` — `idFromName(JSON.stringify([userId, sessionId]))`. The
diff --git a/docs/contributing/architecture/remote-connectors.md b/docs/contributing/architecture/remote-connectors.md
index 50979e4350..5d86c1212e 100644
--- a/docs/contributing/architecture/remote-connectors.md
+++ b/docs/contributing/architecture/remote-connectors.md
@@ -8,7 +8,7 @@ calls to JSON-RPC on the socket.
## URLs and session keys
-`wss:///connectors/u///`
+`wss:///@/connectors//`
Session key = JSON tuple `[userId, kind, instanceId]` where `kind` is lowercase
after trim.
@@ -112,7 +112,7 @@ Source: `packages/shared/src/chat.ts`,
## Connector checklist
1. **Outbound WebSocket** to your connector URL:
- `wss:///connectors/u///`.
+ `wss:///@/connectors//`.
2. **Hello first** with matching **`connectorKind`** + **`connectorId`** and a
**valid `sharedSecret`** for that `kind:instanceId` pair.
3. Implement **`tools/list`** and **`tools/call`** on the socket via
diff --git a/docs/contributing/architecture/request-lifecycle.md b/docs/contributing/architecture/request-lifecycle.md
index 5a23e3bc07..dbcce79454 100644
--- a/docs/contributing/architecture/request-lifecycle.md
+++ b/docs/contributing/architecture/request-lifecycle.md
@@ -39,8 +39,8 @@ Requests are handled in this order:
6. Remote connector session endpoints (internal-only Worker routes that proxy
WebSocket upgrades and JSON-RPC helper requests to the remote connector
session Durable Object):
- - `/connectors/:kind/:instanceId...` — **`kind`** + instance (session key
- `kind:instanceId`)
+ - `/@{username}/connectors/:kind/:instanceId...` — username + **`kind`** +
+ instance
See [Remote connectors](./remote-connectors.md).
diff --git a/docs/contributing/package-invocation-api.md b/docs/contributing/package-invocation-api.md
index 84da3521c8..e4104874e7 100644
--- a/docs/contributing/package-invocation-api.md
+++ b/docs/contributing/package-invocation-api.md
@@ -16,17 +16,13 @@ socket lifecycle.
## Endpoint
-`POST /api/package-invocations/:packageIdOrKodyId/:exportName`
+`POST /@:username/api/package-invocations/:kodyId/:exportName`
Examples:
-- `/api/package-invocations/pkg_123/dispatch-message-created`
-- `/api/package-invocations/discord-gateway/dispatch-message-created`
+- `/@kent/api/package-invocations/discord-gateway/dispatch-message-created`
-The path accepts either:
-
-- the saved package id
-- the package `kody.id`
+The path uses the owner's username and the package `kody.id`.
The export name is normalized to package export form, so
`dispatch-message-created` resolves as `./dispatch-message-created`.
@@ -193,7 +189,7 @@ curl --fail --silent \
-X POST \
-H "Authorization: Bearer $PACKAGE_INVOCATION_TOKEN" \
-H "Content-Type: application/json" \
- "https://kody.example.com/api/package-invocations/discord-gateway/dispatch-message-created" \
+ "https://kody.example.com/@kent/api/package-invocations/discord-gateway/dispatch-message-created" \
-d '{
"params": {
"messageId": "123",
diff --git a/docs/contributing/security.md b/docs/contributing/security.md
index 55665caf20..d0f05fdf85 100644
--- a/docs/contributing/security.md
+++ b/docs/contributing/security.md
@@ -6,8 +6,8 @@ audit for findings and rationale.
## Public connector routes are WebSocket-only
The Worker entrypoint (`packages/worker/src/index.ts`) only forwards user-scoped
-connector route requests (`/connectors/u/{userId}/{kind}/{instanceId}`) when the
-request carries a `WebSocket` upgrade header. Non-upgrade HTTP requests and
+connector route requests (`/@{username}/connectors/{kind}/{instanceId}`) when
+the request carries a `WebSocket` upgrade header. Non-upgrade HTTP requests and
unmatched `/connectors/*` paths are rejected with `404` before reaching static
assets or the Durable Object.
diff --git a/e2e/remote-connectors.spec.ts b/e2e/remote-connectors.spec.ts
index f3850fd286..acb615a9f6 100644
--- a/e2e/remote-connectors.spec.ts
+++ b/e2e/remote-connectors.spec.ts
@@ -25,7 +25,7 @@ test('remote connector form reloads, reveals, and generates shared secrets', asy
await page.getByLabel('Kind').fill(kind)
await page.getByLabel('Instance ID').fill(instanceId)
const connectorUrl = page.getByText(
- new RegExp(`/connectors/u/[^/]+/${kind}/${instanceId}$`),
+ new RegExp(`/@[^/]+/connectors/${kind}/${instanceId}$`),
)
await expect(connectorUrl).toBeVisible()
await page.getByRole('textbox', { name: 'Shared secret' }).fill(sharedSecret)
@@ -53,7 +53,7 @@ test('remote connector form reloads, reveals, and generates shared secrets', asy
await page.getByRole('button', { name: /copy connector url/i }).click()
await expect
.poll(() => page.evaluate(() => navigator.clipboard.readText()))
- .toMatch(new RegExp(`/connectors/u/[^/]+/${kind}/${instanceId}$`))
+ .toMatch(new RegExp(`/@[^/]+/connectors/${kind}/${instanceId}$`))
await page.getByRole('button', { name: 'Show shared secret' }).click()
await expect(sharedSecretInput).toHaveAttribute('type', 'text')
diff --git a/packages/shared/src/chat.ts b/packages/shared/src/chat.ts
index 44b148466b..366bfc3504 100644
--- a/packages/shared/src/chat.ts
+++ b/packages/shared/src/chat.ts
@@ -34,6 +34,7 @@ const remoteConnectorInstanceIdFieldSchema = createSchema(
export const mcpUserContextSchema = object({
userId: string(),
email: string(),
+ username: optional(string()),
displayName: string(),
})
diff --git a/packages/shared/src/public-urls.ts b/packages/shared/src/public-urls.ts
new file mode 100644
index 0000000000..5946eff4f0
--- /dev/null
+++ b/packages/shared/src/public-urls.ts
@@ -0,0 +1,26 @@
+export function buildUsernamePathPrefix(username: string) {
+ return `/@${encodeURIComponent(username.trim())}`
+}
+
+export function buildPackageAppPath(input: {
+ username: string
+ kodyId: string
+ restPath?: string | null
+}) {
+ const restPath = input.restPath?.trim()
+ const suffix = restPath
+ ? `/${restPath.replace(/^\/+/, '')}`
+ : ''
+ return `${buildUsernamePathPrefix(input.username)}/packages/${encodeURIComponent(
+ input.kodyId.trim(),
+ )}${suffix}`
+}
+
+export function buildPackageAppUrl(input: {
+ origin: string
+ username: string
+ kodyId: string
+ restPath?: string | null
+}) {
+ return `${input.origin.trim().replace(/\/+$/, '')}${buildPackageAppPath(input)}`
+}
diff --git a/packages/shared/src/remote-connectors.ts b/packages/shared/src/remote-connectors.ts
index ffdff6b314..8c790cfe7d 100644
--- a/packages/shared/src/remote-connectors.ts
+++ b/packages/shared/src/remote-connectors.ts
@@ -1,5 +1,6 @@
import { type InferOutput } from 'remix/data-schema'
import { type mcpCallerContextSchema } from './chat.ts'
+import { buildUsernamePathPrefix } from './public-urls.ts'
type McpCallerContext = InferOutput
@@ -17,21 +18,20 @@ export function normalizeRemoteConnectorInstanceId(instanceId: string): string {
}
export function userScopedConnectorIngressPath(input: {
- userId: string
+ username: string
kind: string
instanceId: string
}) {
- const userId = encodeURIComponent(input.userId.trim())
const kind = encodeURIComponent(normalizeRemoteConnectorKind(input.kind))
const instanceId = encodeURIComponent(
normalizeRemoteConnectorInstanceId(input.instanceId),
)
- return `/connectors/u/${userId}/${kind}/${instanceId}`
+ return `${buildUsernamePathPrefix(input.username)}/connectors/${kind}/${instanceId}`
}
export function userScopedConnectorWebSocketUrl(input: {
origin: string
- userId: string
+ username: string
kind: string
instanceId: string
}) {
diff --git a/packages/worker/client/routes/account-remote-connectors.tsx b/packages/worker/client/routes/account-remote-connectors.tsx
index 06aa7eb23e..67da1ad431 100644
--- a/packages/worker/client/routes/account-remote-connectors.tsx
+++ b/packages/worker/client/routes/account-remote-connectors.tsx
@@ -40,7 +40,7 @@ type RemoteConnectorListItem = {
type AccountRemoteConnectorsPayload = {
ok: true
email: string
- userId: string
+ username: string
connectorUrlOrigin: string
connectors: Array
selectedConnectorId?: string
@@ -293,7 +293,7 @@ export function AccountRemoteConnectorsRoute(handle: Handle) {
let status: AccountStatus = 'loading'
let saveState: 'idle' | 'saving' | 'deleting' = 'idle'
let email = ''
- let userId = ''
+let username = ''
let connectorUrlOrigin = ''
let connectors: Array = []
let editorState = createEmptyEditorState()
@@ -344,7 +344,7 @@ export function AccountRemoteConnectorsRoute(handle: Handle) {
function applyPayload(payload: AccountRemoteConnectorsPayload) {
email = payload.email
- userId = payload.userId
+ username = payload.username
connectorUrlOrigin = payload.connectorUrlOrigin
connectors = payload.connectors
deleteConfirm = false
@@ -529,11 +529,11 @@ export function AccountRemoteConnectorsRoute(handle: Handle) {
}
function getEditorConnectorUrl() {
- if (!userId || !connectorUrlOrigin) return null
+ if (!username || !connectorUrlOrigin) return null
if (!editorState.kind.trim() || !editorState.instanceId.trim()) return null
return userScopedConnectorWebSocketUrl({
origin: connectorUrlOrigin,
- userId,
+ username,
kind: editorState.kind,
instanceId: editorState.instanceId,
})
@@ -797,7 +797,7 @@ export function AccountRemoteConnectorsRoute(handle: Handle) {
)}
- Includes your MCP user ID {userId} so connector
+ Includes your username {username} so connector
sessions stay isolated to your account.
diff --git a/packages/worker/src/app/authenticated-user.ts b/packages/worker/src/app/authenticated-user.ts
index 316f95d782..fadefe4e12 100644
--- a/packages/worker/src/app/authenticated-user.ts
+++ b/packages/worker/src/app/authenticated-user.ts
@@ -59,6 +59,7 @@ export async function readAuthenticatedAppUser(request: Request, env: Env) {
mcpUser: {
userId: emailBasedUserId,
email: userRecord.email,
+ username: userRecord.username,
displayName,
},
} satisfies AuthenticatedAppUser
diff --git a/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts b/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
index f0d769cd4c..d69571e280 100644
--- a/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
+++ b/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
@@ -4,12 +4,14 @@ const mockModule = vi.hoisted(() => ({
readAuthenticatedAppUser: vi.fn(async () => ({
sessionUserId: '42',
userId: 42,
+ username: 'test-user',
email: 'user@example.com',
displayName: 'user',
artifactOwnerIds: [],
mcpUser: {
userId: 'stable-user-1',
email: 'user@example.com',
+ username: 'test-user',
displayName: 'user',
},
})),
@@ -95,7 +97,7 @@ test('remote connector settings API lists settings with plaintext secrets', asyn
expect(JSON.parse(text)).toEqual({
ok: true,
email: 'user@example.com',
- userId: 'stable-user-1',
+ username: 'test-user',
connectorUrlOrigin: 'wss://example.com',
connectors: [
{
@@ -103,7 +105,7 @@ test('remote connector settings API lists settings with plaintext secrets', asyn
kind: 'lights',
instanceId: 'default',
connectorUrl:
- 'wss://example.com/connectors/u/stable-user-1/lights/default',
+ 'wss://example.com/@test-user/connectors/lights/default',
enabled: true,
attached: true,
hasSharedSecret: true,
@@ -145,7 +147,7 @@ test('remote connector settings API passes submitted secret to save service', as
expect(await response.json()).toEqual({
ok: true,
email: 'user@example.com',
- userId: 'stable-user-1',
+ username: 'test-user',
connectorUrlOrigin: 'wss://example.com',
selectedConnectorId: 'connector-1',
connectors: [
@@ -154,7 +156,7 @@ test('remote connector settings API passes submitted secret to save service', as
kind: 'lights',
instanceId: 'default',
connectorUrl:
- 'wss://example.com/connectors/u/stable-user-1/lights/default',
+ 'wss://example.com/@test-user/connectors/lights/default',
enabled: true,
attached: true,
hasSharedSecret: true,
diff --git a/packages/worker/src/app/handlers/account-remote-connectors.ts b/packages/worker/src/app/handlers/account-remote-connectors.ts
index 1bb201c5cb..a71597ffc2 100644
--- a/packages/worker/src/app/handlers/account-remote-connectors.ts
+++ b/packages/worker/src/app/handlers/account-remote-connectors.ts
@@ -32,7 +32,7 @@ type AccountRemoteConnectorListItem = {
type AccountRemoteConnectorsPayload = {
ok: true
email: string
- userId: string
+ username: string
connectorUrlOrigin: string
connectors: Array
}
@@ -191,17 +191,16 @@ async function buildPayload(input: {
env: input.env,
userId: input.user.mcpUser.userId,
})
- const userId = input.user.mcpUser.userId
return {
ok: true,
email: input.user.email,
- userId,
+ username: input.user.username,
connectorUrlOrigin: input.connectorUrlOrigin,
connectors: connectors.map((connector) => ({
...connector,
connectorUrl: userScopedConnectorWebSocketUrl({
origin: input.connectorUrlOrigin,
- userId,
+ username: input.user.username,
kind: connector.kind,
instanceId: connector.instanceId,
}),
diff --git a/packages/worker/src/app/handlers/package-app.node.test.ts b/packages/worker/src/app/handlers/package-app.node.test.ts
index ac1f083380..5db9ed964a 100644
--- a/packages/worker/src/app/handlers/package-app.node.test.ts
+++ b/packages/worker/src/app/handlers/package-app.node.test.ts
@@ -12,11 +12,13 @@ const mockModule = vi.hoisted(() => ({
setContext: vi.fn(),
},
readAuthenticatedAppUser: vi.fn(async () => ({
+ username: 'test-user',
email: 'user@example.com',
displayName: 'User',
mcpUser: {
userId: 'user-1',
email: 'user@example.com',
+ username: 'test-user',
displayName: 'User',
},
})),
@@ -103,7 +105,7 @@ test('handlePackageAppRequest reports host setup failures with helpful responses
resetMocks()
const response = await handlePackageAppRequest(
- new Request('https://example.com/packages/example/report?tab=errors'),
+ new Request('https://example.com/@test-user/packages/example/report?tab=errors'),
{} as Env,
)
@@ -112,7 +114,7 @@ test('handlePackageAppRequest reports host setup failures with helpful responses
const body = await response.text()
expect(body).toContain('Package app could not be prepared')
expect(body).toContain('@kody/example')
- expect(body).toContain('/packages/example/report')
+ expect(body).toContain('/@test-user/packages/example/report')
expect(mockModule.captureException).toHaveBeenCalledTimes(1)
expect(mockModule.captureException).toHaveBeenCalledWith(expect.any(Error))
expect(mockModule.sentryScope.setLevel).toHaveBeenCalledWith('error')
@@ -153,12 +155,12 @@ test('handlePackageAppRequest reports host setup failures with helpful responses
packageName: '@kody/example',
sourceId: 'source-1',
forwardedPath: '/report',
- hostPath: '/packages/example/report',
+ hostPath: '/@test-user/packages/example/report',
}),
)
const apiResponse = await handlePackageAppRequest(
- new Request('https://example.com/packages/example/api/data', {
+ new Request('https://example.com/@test-user/packages/example/api/data', {
headers: { accept: 'application/json' },
}),
{} as Env,
@@ -175,7 +177,7 @@ test('handlePackageAppRequest reports host setup failures with helpful responses
name: '@kody/example',
kody_id: 'example',
},
- request_path: '/packages/example/api/data',
+ request_path: '/@test-user/packages/example/api/data',
})
})
@@ -208,7 +210,7 @@ test('handlePackageAppRequest does not report package entrypoint failures to Kod
})
const response = await handlePackageAppRequest(
- new Request('https://example.com/packages/example'),
+ new Request('https://example.com/@test-user/packages/example'),
{} as Env,
)
@@ -223,33 +225,16 @@ test('handlePackageAppRequest does not report package entrypoint failures to Kod
test('handlePackageAppRequest routes websocket package paths to realtime session manager', async () => {
resetMocks()
- const request = new Request('https://example.com/packages/example/ws/chat', {
- headers: {
- Upgrade: 'websocket',
+ const request = new Request(
+ 'https://example.com/@test-user/packages/example/ws/chat',
+ {
+ headers: {
+ Upgrade: 'websocket',
+ },
},
- })
-
- const response = await handlePackageAppRequest(request, {} as Env)
-
- expect(response.status).toBe(200)
- expect(mockModule.packageRealtimeConnect).toHaveBeenCalledTimes(1)
- expect(mockModule.packageRealtimeConnect).toHaveBeenCalledWith(
- request,
- 'chat',
)
- expect(mockModule.buildPackageAppWorker).not.toHaveBeenCalled()
-})
-test('handlePackageAppRequest routes websocket paths to realtime session manager when explicitKodyId is provided', async () => {
- resetMocks()
-
- const request = new Request('https://example.com/ws/chat', {
- headers: {
- Upgrade: 'websocket',
- },
- })
-
- const response = await handlePackageAppRequest(request, {} as Env, 'example')
+ const response = await handlePackageAppRequest(request, {} as Env)
expect(response.status).toBe(200)
expect(mockModule.packageRealtimeConnect).toHaveBeenCalledTimes(1)
@@ -260,42 +245,14 @@ test('handlePackageAppRequest routes websocket paths to realtime session manager
expect(mockModule.buildPackageAppWorker).not.toHaveBeenCalled()
})
-test('handlePackageAppRequest preserves root forwarding for non-websocket explicitKodyId requests', async () => {
+test('handlePackageAppRequest returns not found when the URL username does not match the signed-in user', async () => {
resetMocks()
- mockModule.loadPackageSourceBySourceId.mockResolvedValueOnce({
- source: {
- published_commit: 'commit-1',
- manifest_path: 'package.json',
- source_root: '/',
- },
- files: {
- 'package.json': JSON.stringify({
- name: '@kody/example',
- kody: { id: 'example', app: { entry: 'app.js' } },
- }),
- 'app.js': 'export default {}',
- },
- })
- const entrypointFetch = vi.fn(async (forwardedRequest: Request) => {
- return Response.json({ pathname: new URL(forwardedRequest.url).pathname })
- })
- mockModule.buildPackageAppWorker.mockResolvedValueOnce({
- entrypointName: 'entry',
- stub: {
- getEntrypoint: () => ({
- fetch: entrypointFetch,
- }),
- },
- })
-
const response = await handlePackageAppRequest(
- new Request('https://example.com/custom/path'),
+ new Request('https://example.com/@other-user/packages/example'),
{} as Env,
- 'example',
)
- expect(response.status).toBe(200)
- await expect(response.json()).resolves.toEqual({ pathname: '/' })
- expect(entrypointFetch).toHaveBeenCalledTimes(1)
+ expect(response.status).toBe(404)
+ expect(mockModule.getSavedPackageByKodyId).not.toHaveBeenCalled()
})
diff --git a/packages/worker/src/app/handlers/package-app.ts b/packages/worker/src/app/handlers/package-app.ts
index 441f5a07f4..cb0be32a37 100644
--- a/packages/worker/src/app/handlers/package-app.ts
+++ b/packages/worker/src/app/handlers/package-app.ts
@@ -4,6 +4,7 @@ import { createHtmlResponse } from 'remix/response/html'
import { readAuthenticatedAppUser } from '#app/authenticated-user.ts'
import { redirectToLogin } from '#app/auth-redirect.ts'
import { getAppBaseUrl } from '#app/app-base-url.ts'
+import { getUsernameValidationError } from '#app/username.ts'
import { getSavedPackageByKodyId } from '#worker/package-registry/repo.ts'
import { loadPackageSourceBySourceId } from '#worker/package-registry/source.ts'
import {
@@ -15,21 +16,30 @@ import { wantsJson } from '#worker/utils.ts'
function parsePackageAppPath(pathname: string) {
const parts = pathname.split('/').filter(Boolean)
- if (parts[0] !== 'packages') return null
- const rawKodyId = parts[1]?.trim()
+ const rawUsername = parts[0]?.startsWith('@') ? parts[0].slice(1) : ''
+ if (!rawUsername || parts[1] !== 'packages') return null
+ const rawKodyId = parts[2]?.trim()
if (!rawKodyId) return null
+ let username: string
let kodyId: string
try {
+ username = decodeURIComponent(rawUsername)
kodyId = decodeURIComponent(rawKodyId)
} catch {
return null
}
+ if (getUsernameValidationError(username)) return null
return {
+ username,
kodyId,
- restPath: parts.length > 2 ? `/${parts.slice(2).join('/')}` : '/',
+ restPath: parts.length > 3 ? `/${parts.slice(3).join('/')}` : '/',
}
}
+export function isPackageAppRequestPath(pathname: string) {
+ return parsePackageAppPath(pathname) !== null
+}
+
function parsePackageRealtimePath(restPath: string) {
const parts = restPath.split('/').filter(Boolean)
if (parts[0] !== 'ws') return null
@@ -228,24 +238,22 @@ function createPackageAppErrorResponse(input: {
export async function handlePackageAppRequest(
request: Request,
env: Env,
- explicitKodyId?: string | null,
) {
const requestUrl = new URL(request.url)
const packagePath = parsePackageAppPath(requestUrl.pathname)
- const kodyId = explicitKodyId?.trim() || packagePath?.kodyId || null
- if (!kodyId) {
+ if (!packagePath) {
return new Response('Saved package app not found.', { status: 404 })
}
- const packageRealtimeRestPath =
- packagePath?.kodyId === kodyId
- ? packagePath.restPath
- : requestUrl.pathname || '/'
- const forwardedPackageRestPath =
- packagePath?.kodyId === kodyId ? packagePath.restPath : '/'
+ const { kodyId } = packagePath
+ const packageRealtimeRestPath = packagePath.restPath
+ const forwardedPackageRestPath = packagePath.restPath
const user = await readAuthenticatedAppUser(request, env)
if (!user) {
return redirectToLogin(request)
}
+ if (user.username !== packagePath.username) {
+ return new Response('Saved package app not found.', { status: 404 })
+ }
const savedPackage = await getSavedPackageByKodyId(env.APP_DB, {
userId: user.mcpUser.userId,
kodyId,
@@ -302,6 +310,7 @@ export async function handlePackageAppRequest(
userId: user.mcpUser.userId,
email: user.email,
displayName: user.displayName,
+ username: user.username,
},
packageId: savedPackage.id,
})
diff --git a/packages/worker/src/app/router.ts b/packages/worker/src/app/router.ts
index 4acd8551f5..dff3b3f897 100644
--- a/packages/worker/src/app/router.ts
+++ b/packages/worker/src/app/router.ts
@@ -35,7 +35,7 @@ export function createAppRouter(appEnv: AppEnv) {
const router = createRouter({
middleware: [],
async defaultHandler() {
- return render(Layout({}))
+ return render(Layout({}), { status: 404 })
},
})
diff --git a/packages/worker/src/app/user-lookup.ts b/packages/worker/src/app/user-lookup.ts
new file mode 100644
index 0000000000..75bd245b27
--- /dev/null
+++ b/packages/worker/src/app/user-lookup.ts
@@ -0,0 +1,31 @@
+import { getUsernameValidationError } from '#app/username.ts'
+import { createDb, usersTable } from '#worker/db.ts'
+import { createStableUserIdFromEmail } from '#worker/user-id.ts'
+
+export type PublicUserIdentity = {
+ userId: number
+ username: string
+ email: string
+ mcpUserId: string
+}
+
+export async function findPublicUserIdentityByUsername(input: {
+ db: D1Database
+ username: string
+}): Promise {
+ const username = input.username.trim()
+ if (getUsernameValidationError(username)) return null
+
+ const db = createDb(input.db)
+ const userRecord = await db.findOne(usersTable, {
+ where: { username },
+ })
+ if (!userRecord) return null
+
+ return {
+ userId: userRecord.id,
+ username: userRecord.username,
+ email: userRecord.email,
+ mcpUserId: await createStableUserIdFromEmail(userRecord.email),
+ }
+}
diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts
index ca67da7cd8..89048ce4be 100644
--- a/packages/worker/src/index.ts
+++ b/packages/worker/src/index.ts
@@ -45,9 +45,13 @@ import {
parseUserScopedConnectorRoutePath,
userScopedConnectorSessionKey,
} from './remote-connector/connector-session-key.ts'
-import { handlePackageAppRequest } from '#app/handlers/package-app.ts'
+import {
+ handlePackageAppRequest,
+ isPackageAppRequestPath,
+} from '#app/handlers/package-app.ts'
import { PackageAppRuntimeBridge } from '#worker/package-runtime/package-app.ts'
import { handleInboundEmail } from '#worker/email/inbound.ts'
+import { findPublicUserIdentityByUsername } from '#app/user-lookup.ts'
export {
RepoSession,
@@ -186,7 +190,7 @@ const appHandler = withCors({
return handleGeneratedUiApiRequest(request, env)
}
- if (url.pathname.startsWith('/packages/')) {
+ if (isPackageAppRequestPath(url.pathname)) {
return handlePackageAppRequest(request, env)
}
@@ -197,8 +201,15 @@ const appHandler = withCors({
if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
return new Response('Not Found', { status: 404 })
}
+ const routeUser = await findPublicUserIdentityByUsername({
+ db: env.APP_DB,
+ username: userScopedConnectorRoute.username,
+ })
+ if (!routeUser) {
+ return new Response('Not Found', { status: 404 })
+ }
const sessionKey = userScopedConnectorSessionKey({
- userId: userScopedConnectorRoute.userId,
+ userId: routeUser.mcpUserId,
kind: userScopedConnectorRoute.kind,
instanceId: userScopedConnectorRoute.instanceId,
})
@@ -211,7 +222,7 @@ const appHandler = withCors({
forwardRequest.headers.set('X-Kody-Connector-Session-Key', sessionKey)
forwardRequest.headers.set(
'X-Kody-Connector-User-Id',
- userScopedConnectorRoute.userId,
+ routeUser.mcpUserId,
)
return stub.fetch(forwardRequest)
}
diff --git a/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts b/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts
index ca3c901490..982544c310 100644
--- a/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts
+++ b/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts
@@ -46,6 +46,8 @@ async function getOpenGeneratedUiHandler() {
user: {
userId: 'user-123',
email: 'user@example.com',
+ username: 'test-user',
+ displayName: 'test-user',
},
})),
requireDomain: vi.fn(() => 'https://example.com'),
@@ -91,6 +93,6 @@ test('open_generated_ui reopens saved package apps by kody_id', async () => {
)
expect(response.structuredContent).toMatchObject({
appId: 'package-123',
- hostedUrl: 'https://example.com/packages/observed-package',
+ hostedUrl: 'https://example.com/@test-user/packages/observed-package',
})
})
diff --git a/packages/worker/src/mcp/tools/open-generated-ui.ts b/packages/worker/src/mcp/tools/open-generated-ui.ts
index 1c802beaa7..a76631a3a2 100644
--- a/packages/worker/src/mcp/tools/open-generated-ui.ts
+++ b/packages/worker/src/mcp/tools/open-generated-ui.ts
@@ -5,6 +5,7 @@ import { generatedUiRuntimeResourceUri } from '#mcp/apps/generated-ui-runtime-ht
import { createGeneratedUiAppSession } from '#mcp/generated-ui-app-session.ts'
import { getSavedPackageByKodyId } from '#worker/package-registry/repo.ts'
import { type McpRegistrationAgent } from '#mcp/mcp-registration-agent.ts'
+import { buildPackageAppUrl } from '@kody-internal/shared/public-urls.ts'
import {
conversationIdInputField,
memoryContextInputField,
@@ -118,8 +119,16 @@ export async function registerOpenGeneratedUiTool(agent: McpRegistrationAgent) {
)
}
}
+ const username = callerContext.user?.username
+ if (savedPackage && !username) {
+ throw new Error('Username is required to open saved package apps.')
+ }
const hostedUrl = savedPackage
- ? `${agent.requireDomain()}/packages/${encodeURIComponent(savedPackage.kodyId)}`
+ ? buildPackageAppUrl({
+ origin: agent.requireDomain(),
+ username,
+ kodyId: savedPackage.kodyId,
+ })
: null
const inlineSourceCode = savedPackage
? [
diff --git a/packages/worker/src/mcp/tools/search-format.node.test.ts b/packages/worker/src/mcp/tools/search-format.node.test.ts
index 71761c6c18..32566e5e05 100644
--- a/packages/worker/src/mcp/tools/search-format.node.test.ts
+++ b/packages/worker/src/mcp/tools/search-format.node.test.ts
@@ -322,7 +322,7 @@ test('entity detail formatting includes package app, export, and job metadata',
id: 'observed-package',
title: '@kody/observed-package',
description: 'Observed package with an app surface.',
- hostedUrl: 'http://localhost/packages/observed-package',
+ hostedUrl: 'http://localhost/@test-user/packages/observed-package',
record: {
id: 'package-123',
userId: 'user-123',
@@ -387,7 +387,7 @@ export declare function fetch(request: Request): Promise
entityRef: 'observed-package:package',
usage: 'open_generated_ui({ kody_id: "observed-package" })',
hasApp: true,
- hostedUrl: 'http://localhost/packages/observed-package',
+ hostedUrl: 'http://localhost/@test-user/packages/observed-package',
appEntry: './src/app.ts',
})
expect(packageDetail.structured).toMatchObject({
@@ -530,6 +530,7 @@ export declare function launch(input: LaunchCursorCloudAgentInput): Promise {
const [packageMatch] = toSlimStructuredMatches({
baseUrl: 'http://localhost',
+ username: 'test-user',
matches: [
{
type: 'package',
@@ -563,7 +564,7 @@ test('package search formatting keeps runnable package actions in structured out
openGeneratedUiUsage: 'open_generated_ui({ kody_id: "spotify-playback" })',
tags: ['spotify', 'playback'],
hasApp: true,
- hostedUrl: 'http://localhost/packages/spotify-playback',
+ hostedUrl: 'http://localhost/@test-user/packages/spotify-playback',
readmeSnippet: {
path: 'README.md',
snippet:
@@ -609,6 +610,7 @@ test('package search formatting surfaces matched action recipes compactly', () =
})
const [packageMatch] = toSlimStructuredMatches({
baseUrl: 'http://localhost',
+ username: 'test-user',
matches: [
{
type: 'package',
diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts
index 41bbb616b6..3406d1c2fa 100644
--- a/packages/worker/src/mcp/tools/search-format.ts
+++ b/packages/worker/src/mcp/tools/search-format.ts
@@ -18,6 +18,7 @@ import {
escapeMarkdownText,
formatMarkdownInlineCode,
} from './markdown-safety.ts'
+import { buildPackageAppUrl } from '@kody-internal/shared/public-urls.ts'
export type SearchEntityType =
| 'capability'
@@ -405,8 +406,15 @@ export type SearchMatch =
type: 'retriever_result'
})
-function buildPackageHostedUrl(baseUrl: string, kodyId: string) {
- return `${baseUrl.replace(/\/+$/, '')}/packages/${encodeURIComponent(kodyId)}`
+function buildPackageHostedUrl(baseUrl: string, username: string, kodyId: string) {
+ return buildPackageAppUrl({ origin: baseUrl, username, kodyId })
+}
+
+function requireUsernameForHostedPackageUrl(username: string | null) {
+ if (!username) {
+ throw new Error('Username is required to build hosted package app URLs.')
+ }
+ return username
}
function buildEntityRef(id: string, type: SearchEntityType) {
@@ -612,6 +620,7 @@ function formatMatchListItem(match: SearchMatch, index: number) {
export function toSlimStructuredMatches(input: {
matches: Array
baseUrl: string
+ username?: string | null
}): Array {
return input.matches.map((match) => {
if (match.type === 'capability') {
@@ -683,7 +692,11 @@ export function toSlimStructuredMatches(input: {
tags: match.tags,
hasApp: match.hasApp,
hostedUrl: match.hasApp
- ? buildPackageHostedUrl(input.baseUrl, match.kodyId)
+ ? buildPackageHostedUrl(
+ input.baseUrl,
+ requireUsernameForHostedPackageUrl(input.username),
+ match.kodyId,
+ )
: null,
readmeSnippet: match.readmeSnippet
? {
diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts
index be6c04944c..547bb5e849 100644
--- a/packages/worker/src/mcp/tools/search.ts
+++ b/packages/worker/src/mcp/tools/search.ts
@@ -46,6 +46,7 @@ import {
type RemoteConnectorStatus,
} from '#worker/remote-connector/status.ts'
import { type McpCallerContext } from '@kody-internal/shared/chat.ts'
+import { buildPackageAppUrl } from '@kody-internal/shared/public-urls.ts'
import { normalizeRemoteConnectorRefs } from '@kody-internal/shared/remote-connectors.ts'
import { type PackageRetrieverSurfaceResult } from '#worker/package-retrievers/types.ts'
import {
@@ -170,6 +171,13 @@ type SearchUnifiedResult = {
guidance?: string
}
+function requireUsernameForHostedPackageUrl(username: string | null) {
+ if (!username) {
+ throw new Error('Username is required to build hosted package app URLs.')
+ }
+ return username
+}
+
function flattenReferencedTypeFields(
referencedTypes:
| ReadonlyArray<{ name: string; definition: string }>
@@ -1674,7 +1682,13 @@ async function resolveEntityDetail(input: {
manifest: loaded.manifest,
files: loaded.files,
hostedUrl: record.hasApp
- ? `${input.callerContext.baseUrl}/packages/${encodeURIComponent(record.kodyId)}`
+ ? buildPackageAppUrl({
+ origin: input.callerContext.baseUrl,
+ username: requireUsernameForHostedPackageUrl(
+ input.callerContext.user?.username ?? null,
+ ),
+ kodyId: record.kodyId,
+ })
: null,
}
}
@@ -2074,6 +2088,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) {
matches: toSlimStructuredMatches({
matches: trimmedPayload.matches,
baseUrl,
+ username: callerContext.user?.username ?? null,
}),
}
const timing = finishToolTiming(timingStart)
diff --git a/packages/worker/src/oauth-handlers.ts b/packages/worker/src/oauth-handlers.ts
index 0428e773cf..e043d1728a 100644
--- a/packages/worker/src/oauth-handlers.ts
+++ b/packages/worker/src/oauth-handlers.ts
@@ -32,6 +32,7 @@ export const oauthScopes: Array = ['profile', 'email']
type OAuthProps = {
userId: string
email: string
+ username: string
displayName: string
}
@@ -628,6 +629,7 @@ export async function handleAuthorizeRequest(
}
let approvedEmail = ''
+ let approvedUsername = ''
if (hasFormCredentials) {
const db = createDb(env.APP_DB)
const userRecord = await db.findOne(usersTable, {
@@ -653,14 +655,23 @@ export async function handleAuthorizeRequest(
return respondAuthorizeError(request, 'Invalid email or password.')
}
approvedEmail = normalizedEmail
+ approvedUsername = userRecord.username
} else if (sessionEmail) {
+ const db = createDb(env.APP_DB)
+ const userRecord = await db.findOne(usersTable, {
+ where: { email: sessionEmail },
+ })
+ if (!userRecord) {
+ return respondAuthorizeError(request, 'Signed-in user not found.', 401)
+ }
approvedEmail = sessionEmail
+ approvedUsername = userRecord.username
}
const resolvedScopes = resolveScopes(authRequest.scope)
if (Array.isArray(resolvedScopes)) {
const userId = await createStableUserIdFromEmail(approvedEmail)
- const displayName = approvedEmail.split('@')[0] || 'user'
+ const displayName = approvedUsername
const { redirectTo } = await helpers.completeAuthorization({
request: authRequest,
userId,
@@ -672,6 +683,7 @@ export async function handleAuthorizeRequest(
props: {
userId,
email: approvedEmail,
+ username: approvedUsername,
displayName,
},
})
diff --git a/packages/worker/src/package-invocations/http.ts b/packages/worker/src/package-invocations/http.ts
index 12259c5439..1c56cf4c25 100644
--- a/packages/worker/src/package-invocations/http.ts
+++ b/packages/worker/src/package-invocations/http.ts
@@ -1,5 +1,6 @@
import { getRequestIp, logAuditEvent } from '#app/audit-log.ts'
import { getAppBaseUrl } from '#app/app-base-url.ts'
+import { findPublicUserIdentityByUsername } from '#app/user-lookup.ts'
import {
invokePackageExport,
type PackageInvocationTokenScope,
@@ -10,8 +11,6 @@ import {
updatePackageInvocationTokenLastUsed,
} from './repo.ts'
-const packageInvocationPathPrefix = '/api/package-invocations/'
-
type PackageInvocationRequestBody = {
params?: Record
idempotencyKey?: string
@@ -82,20 +81,23 @@ function decodePathComponent(value: string) {
}
function parsePackageInvocationPath(pathname: string) {
- if (!pathname.startsWith(packageInvocationPathPrefix)) {
- return null
- }
- const rest = pathname.slice(packageInvocationPathPrefix.length)
- const parts = rest.split('/')
- if (parts.length < 2) {
+ const parts = pathname.split('/').filter(Boolean)
+ if (
+ parts.length < 5 ||
+ !parts[0]?.startsWith('@') ||
+ parts[0].length <= 1 ||
+ parts[1] !== 'api' ||
+ parts[2] !== 'package-invocations'
+ ) {
return null
}
- const packageIdOrKodyId = decodePathComponent(parts[0] ?? '')
- const exportName = decodePathComponent(parts.slice(1).join('/'))
- if (!packageIdOrKodyId || !exportName) {
+ const username = decodePathComponent(parts[0].slice(1))
+ const kodyId = decodePathComponent(parts[3] ?? '')
+ const exportName = decodePathComponent(parts.slice(4).join('/'))
+ if (!username || !kodyId || !exportName) {
return null
}
- return { packageIdOrKodyId, exportName }
+ return { username, kodyId, exportName }
}
async function resolveTokenScope(input: {
@@ -168,7 +170,7 @@ async function readRequestBody(
}
export function isPackageInvocationApiRequest(pathname: string) {
- return pathname.startsWith(packageInvocationPathPrefix)
+ return parsePackageInvocationPath(pathname) !== null
}
export async function handlePackageInvocationApiRequest(
@@ -220,6 +222,22 @@ export async function handlePackageInvocationApiRequest(
})
return unauthorizedResponse('Invalid package invocation token.')
}
+ const routeUser = await findPublicUserIdentityByUsername({
+ db: env.APP_DB,
+ username: route.username,
+ })
+ if (!routeUser || routeUser.mcpUserId !== tokenScope.userId) {
+ logPackageInvocationAudit(ctx, {
+ category: 'oauth',
+ action: 'package_invoke',
+ result: 'failure',
+ email: tokenScope.email,
+ ip: requestIp,
+ path: new URL(request.url).pathname,
+ reason: 'username_token_mismatch',
+ })
+ return notFoundResponse()
+ }
const parsedBody = await readRequestBody(request)
if (!parsedBody.ok) {
return parsedBody.response
@@ -289,7 +307,7 @@ export async function handlePackageInvocationApiRequest(
}),
token: tokenScope,
request: {
- packageIdOrKodyId: route.packageIdOrKodyId,
+ packageIdOrKodyId: route.kodyId,
exportName: route.exportName,
params: body.params,
idempotencyKey: body.idempotencyKey,
diff --git a/packages/worker/src/package-invocations/http.workers.test.ts b/packages/worker/src/package-invocations/http.workers.test.ts
index 0387dd0667..8e00af19a1 100644
--- a/packages/worker/src/package-invocations/http.workers.test.ts
+++ b/packages/worker/src/package-invocations/http.workers.test.ts
@@ -5,6 +5,7 @@ import {
isPackageInvocationApiRequest,
} from './http.ts'
import { hashPackageInvocationBearerToken } from './repo.ts'
+import { createStableUserIdFromEmail } from '#worker/user-id.ts'
const invocationMockModule = vi.hoisted(() => ({
invokePackageExport: vi.fn(),
@@ -33,10 +34,11 @@ async function createEnv(
touchError?: Error
} = {},
) {
+ const tokenUserId = await createStableUserIdFromEmail('me@example.com')
const tokenRows = [
{
id: 'discord-gateway',
- user_id: 'user-123',
+ user_id: tokenUserId,
token_hash: await hashPackageInvocationBearerToken('private-token-123'),
name: 'Discord gateway',
email: 'me@example.com',
@@ -63,6 +65,19 @@ async function createEnv(
bind(...params: Array) {
return {
async first>() {
+ if (query.includes('FROM users')) {
+ const username = String(params[0] ?? '')
+ return (username === 'me'
+ ? {
+ id: 1,
+ username: 'me',
+ email: 'me@example.com',
+ password_hash: 'hash',
+ created_at: '2026-04-27T00:00:00.000Z',
+ updated_at: '2026-04-27T00:00:00.000Z',
+ }
+ : null) as T | null
+ }
if (query.includes('FROM package_invocation_tokens')) {
const tokenHash = String(params[0] ?? '')
return (tokenRows.find(
@@ -137,7 +152,7 @@ function createContext() {
test('isPackageInvocationApiRequest matches the external package invocation route', () => {
expect(
isPackageInvocationApiRequest(
- '/api/package-invocations/discord-gateway/dispatch-message-created',
+ '/@me/api/package-invocations/discord-gateway/dispatch-message-created',
),
).toBe(true)
expect(isPackageInvocationApiRequest('/api/me')).toBe(false)
@@ -146,7 +161,7 @@ test('isPackageInvocationApiRequest matches the external package invocation rout
test('package invocation API returns 401 when bearer token is missing', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -173,7 +188,7 @@ test('package invocation API returns 401 when bearer token is missing', async ()
test('package invocation API returns 401 for invalid private tokens', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -200,7 +215,7 @@ test('package invocation API returns 401 for invalid private tokens', async () =
test('package invocation API fails closed when token touch loses revocation race', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -228,7 +243,7 @@ test('package invocation API fails closed when token touch loses revocation race
test('package invocation API fails closed when token scope JSON is malformed', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -260,7 +275,7 @@ test('package invocation API fails closed when token scope JSON is malformed', a
test('package invocation API validates the JSON body shape', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -300,9 +315,10 @@ test('package invocation API invokes the package export with the scoped token co
})
const ctx = createContext()
+ const expectedUserId = await createStableUserIdFromEmail('me@example.com')
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -326,7 +342,7 @@ test('package invocation API invokes the package export with the scoped token co
baseUrl: 'https://example.com',
token: {
tokenId: 'discord-gateway',
- userId: 'user-123',
+ userId: expectedUserId,
email: 'me@example.com',
displayName: 'me',
packageIds: [],
diff --git a/packages/worker/src/package-runtime/package-app.ts b/packages/worker/src/package-runtime/package-app.ts
index d39e01153f..7f3c50af26 100644
--- a/packages/worker/src/package-runtime/package-app.ts
+++ b/packages/worker/src/package-runtime/package-app.ts
@@ -1193,6 +1193,7 @@ export async function createPackageAppCallerContext(input: {
user: {
userId: string
email: string
+ username?: string
displayName?: string
}
packageId: string
@@ -1202,6 +1203,7 @@ export async function createPackageAppCallerContext(input: {
user: {
userId: input.user.userId,
email: input.user.email,
+ username: input.user.username,
displayName: input.user.displayName ?? `package:${input.packageId}`,
},
storageContext: {
diff --git a/packages/worker/src/remote-connector/connector-session-key.node.test.ts b/packages/worker/src/remote-connector/connector-session-key.node.test.ts
index 06d12b8af3..483589c8b1 100644
--- a/packages/worker/src/remote-connector/connector-session-key.node.test.ts
+++ b/packages/worker/src/remote-connector/connector-session-key.node.test.ts
@@ -37,38 +37,38 @@ test('userScopedConnectorSessionKey unambiguously encodes "/" in any segment', (
expect(collidingA).not.toBe(collidingB)
})
-test('userScopedConnectorIngressPath builds /connectors/u/... routes', () => {
+test('userScopedConnectorIngressPath builds username-scoped connector routes', () => {
expect(
userScopedConnectorIngressPath({
- userId: 'user-aaa',
+ username: 'user-aaa',
kind: 'lights',
instanceId: 'living-room',
}),
- ).toBe('/connectors/u/user-aaa/lights/living-room')
+ ).toBe('/@user-aaa/connectors/lights/living-room')
expect(
userScopedConnectorIngressPath({
- userId: 'with space',
+ username: 'with space',
kind: 'CUSTOM',
instanceId: 'a b',
}),
- ).toBe('/connectors/u/with%20space/custom/a%20b')
+ ).toBe('/@with%20space/connectors/custom/a%20b')
})
-test('parseUserScopedConnectorRoutePath extracts userId, kind, and instanceId', () => {
+test('parseUserScopedConnectorRoutePath extracts username, kind, and instanceId', () => {
expect(
parseUserScopedConnectorRoutePath(
- '/connectors/u/user-aaa/lights/default/snapshot',
+ '/@user-aaa/connectors/lights/default/snapshot',
),
).toEqual({
- userId: 'user-aaa',
+ username: 'user-aaa',
kind: 'lights',
instanceId: 'default',
rest: '/snapshot',
})
expect(
- parseUserScopedConnectorRoutePath('/connectors/u/user-bbb/custom/abc'),
+ parseUserScopedConnectorRoutePath('/@user-bbb/connectors/custom/abc'),
).toEqual({
- userId: 'user-bbb',
+ username: 'user-bbb',
kind: 'custom',
instanceId: 'abc',
rest: '',
@@ -77,6 +77,6 @@ test('parseUserScopedConnectorRoutePath extracts userId, kind, and instanceId',
parseUserScopedConnectorRoutePath('/connectors/lights/default'),
).toBeNull()
expect(
- parseUserScopedConnectorRoutePath('/connectors/u/user-aaa/lights'),
+ parseUserScopedConnectorRoutePath('/@user-aaa/connectors/lights'),
).toBeNull()
})
diff --git a/packages/worker/src/remote-connector/connector-session-key.ts b/packages/worker/src/remote-connector/connector-session-key.ts
index ba102f30b1..6544cfa464 100644
--- a/packages/worker/src/remote-connector/connector-session-key.ts
+++ b/packages/worker/src/remote-connector/connector-session-key.ts
@@ -6,7 +6,7 @@ import {
export { userScopedConnectorIngressPath } from '@kody-internal/shared/remote-connectors.ts'
export type UserScopedConnectorRouteMatch = {
- userId: string
+ username: string
kind: string
instanceId: string
rest: string
@@ -43,23 +43,23 @@ export function parseUserScopedConnectorRoutePath(
}
}
if (
- parts.length >= 5 &&
- parts[0] === 'connectors' &&
- parts[1] === 'u' &&
+ parts.length >= 4 &&
+ parts[0]?.startsWith('@') &&
+ parts[0].length > 1 &&
+ parts[1] === 'connectors' &&
parts[2] &&
- parts[3] &&
- parts[4]
+ parts[3]
) {
- const decodedUserId = decodeSegment(parts[2])
- const decodedKind = decodeSegment(parts[3])
- const decodedInstanceId = decodeSegment(parts[4])
- if (!decodedUserId || !decodedKind || !decodedInstanceId) return null
- const userId = decodedUserId.trim()
+ const decodedUsername = decodeSegment(parts[0].slice(1))
+ const decodedKind = decodeSegment(parts[2])
+ const decodedInstanceId = decodeSegment(parts[3])
+ if (!decodedUsername || !decodedKind || !decodedInstanceId) return null
+ const username = decodedUsername.trim()
const kind = decodedKind.trim().toLowerCase()
const instanceId = decodedInstanceId.trim()
- if (!userId || !kind || !instanceId) return null
- const rest = parts.length > 5 ? `/${parts.slice(5).join('/')}` : ''
- return { userId, kind, instanceId, rest }
+ if (!username || !kind || !instanceId) return null
+ const rest = parts.length > 4 ? `/${parts.slice(4).join('/')}` : ''
+ return { username, kind, instanceId, rest }
}
return null
}
diff --git a/packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts b/packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts
index 7bfae0c880..cab2559f6e 100644
--- a/packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts
+++ b/packages/worker/src/remote-connector/remote-connectors-shared.node.test.ts
@@ -34,13 +34,13 @@ test('normalizeRemoteConnectorRefs preserves an empty connector list', () => {
).toEqual([])
})
-test('userScopedConnectorWebSocketUrl builds encoded user-scoped connector URLs', () => {
+test('userScopedConnectorWebSocketUrl builds encoded username-scoped connector URLs', () => {
expect(
userScopedConnectorWebSocketUrl({
origin: 'wss://kody.example.com/',
- userId: 'user aaa',
+ username: 'user-aaa',
kind: 'Lights',
instanceId: 'living room',
}),
- ).toBe('wss://kody.example.com/connectors/u/user%20aaa/lights/living%20room')
+ ).toBe('wss://kody.example.com/@user-aaa/connectors/lights/living%20room')
})
diff --git a/packages/worker/src/remote-connector/session.ts b/packages/worker/src/remote-connector/session.ts
index 9ebeaf7821..2ef4aeb4e8 100644
--- a/packages/worker/src/remote-connector/session.ts
+++ b/packages/worker/src/remote-connector/session.ts
@@ -363,7 +363,7 @@ class RemoteConnectorSessionBase extends DurableObject {
stringifyRemoteConnectorMessage({
type: 'server.error',
message:
- 'Connector ingress is missing the owning user id. Reconfigure the connector to use the /connectors/u/{userId}/{kind}/{instanceId} URL.',
+ 'Connector ingress is missing the owning user id. Reconfigure the connector to use the /@{username}/connectors/{kind}/{instanceId} URL.',
}),
)
ws.close(4002, 'missing-user')
diff --git a/packages/worker/src/security/public-route-hardening.workers.test.ts b/packages/worker/src/security/public-route-hardening.workers.test.ts
index 3303286108..bf1a8b181f 100644
--- a/packages/worker/src/security/public-route-hardening.workers.test.ts
+++ b/packages/worker/src/security/public-route-hardening.workers.test.ts
@@ -17,17 +17,21 @@ async function workerFetch(request: Request): Promise {
}
test('user-scoped connector entrypoints reject unauthenticated HTTP access while allowing WebSocket upgrades', async () => {
+ await env.APP_DB.prepare(
+ `INSERT OR IGNORE INTO users (username, email, password_hash)
+ VALUES ('connector-user', 'connector-user@example.com', 'hash')`,
+ ).run()
const unauthorizedRequests = [
- createRequest('/connectors/u/user-1/lights/default/snapshot'),
- createRequest('/connectors/u/user-1/lights/default/rpc/tools-list', {
+ createRequest('/@connector-user/connectors/lights/default/snapshot'),
+ createRequest('/@connector-user/connectors/lights/default/rpc/tools-list', {
method: 'POST',
}),
- createRequest('/connectors/u/user-1/lights/default/rpc/tools-call', {
+ createRequest('/@connector-user/connectors/lights/default/rpc/tools-call', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test', arguments: {} }),
}),
- createRequest('/connectors/u/user-1/lights/default/rpc/jsonrpc', {
+ createRequest('/@connector-user/connectors/lights/default/rpc/jsonrpc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -42,7 +46,7 @@ test('user-scoped connector entrypoints reject unauthenticated HTTP access while
}
const websocketRequest = createRequest(
- '/connectors/u/user-1/lights/default',
+ '/@connector-user/connectors/lights/default',
{
headers: { Upgrade: 'websocket' },
},
@@ -55,8 +59,8 @@ test('user-scoped connector entrypoints reject unauthenticated HTTP access while
test('unmatched connector ingress paths do not fall through to the SPA shell', async () => {
const requests = [
- createRequest('/connectors/home/default'),
- createRequest('/connectors/home/default', {
+ createRequest('/@connector-user/connectors'),
+ createRequest('/@connector-user/connectors/home', {
headers: { Upgrade: 'websocket' },
}),
]
diff --git a/packages/worker/wrangler.jsonc b/packages/worker/wrangler.jsonc
index f87320d233..baffd2e2a1 100644
--- a/packages/worker/wrangler.jsonc
+++ b/packages/worker/wrangler.jsonc
@@ -99,7 +99,13 @@
"assets": {
"directory": "./public",
"binding": "ASSETS",
- "run_worker_first": ["/connectors/*", "/mcp-apps/*", "/styles.css"],
+ "run_worker_first": [
+ "/@*/api/package-invocations/*",
+ "/@*/connectors/*",
+ "/@*/packages/*",
+ "/mcp-apps/*",
+ "/styles.css",
+ ],
},
"durable_objects": {
"bindings": [
@@ -183,7 +189,13 @@
"assets": {
"directory": "./public",
"binding": "ASSETS",
- "run_worker_first": ["/connectors/*", "/mcp-apps/*", "/styles.css"],
+ "run_worker_first": [
+ "/@*/api/package-invocations/*",
+ "/@*/connectors/*",
+ "/@*/packages/*",
+ "/mcp-apps/*",
+ "/styles.css",
+ ],
},
"durable_objects": {
"bindings": [
@@ -267,7 +279,13 @@
"assets": {
"directory": "./public",
"binding": "ASSETS",
- "run_worker_first": ["/connectors/*", "/mcp-apps/*", "/styles.css"],
+ "run_worker_first": [
+ "/@*/api/package-invocations/*",
+ "/@*/connectors/*",
+ "/@*/packages/*",
+ "/mcp-apps/*",
+ "/styles.css",
+ ],
},
"durable_objects": {
"bindings": [
diff --git a/tools/ci/resource-utils.node.test.ts b/tools/ci/resource-utils.node.test.ts
index a9ec333fa0..9c324deaaf 100644
--- a/tools/ci/resource-utils.node.test.ts
+++ b/tools/ci/resource-utils.node.test.ts
@@ -82,7 +82,9 @@ test('writeGeneratedWranglerConfig copies production asset routing to the deploy
}
}>(generatedConfigText)
- expect(generatedConfig.assets?.run_worker_first).toContain('/connectors/*')
+ expect(generatedConfig.assets?.run_worker_first).toContain(
+ '/@*/connectors/*',
+ )
expect(generatedConfig.assets).toEqual(
generatedConfig.env?.production?.assets,
)
@@ -115,7 +117,9 @@ test('writeGeneratedWranglerConfig copies preview asset routing to the deployed
}
}>(generatedConfigText)
- expect(generatedConfig.assets?.run_worker_first).toContain('/connectors/*')
+ expect(generatedConfig.assets?.run_worker_first).toContain(
+ '/@*/connectors/*',
+ )
expect(generatedConfig.assets).toEqual(generatedConfig.env?.preview?.assets)
} finally {
await rm(tempDir, { force: true, recursive: true })
From 1b133572f286133855506b87651a688d7eeafa86 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 19:01:49 +0000
Subject: [PATCH 2/7] Finish username URL cutover
Co-authored-by: Kent C. Dodds
---
packages/shared/src/public-urls.ts | 4 +-
.../routes/account-remote-connectors.tsx | 2 +-
.../account-remote-connectors.node.test.ts | 6 +-
.../src/app/handlers/package-app.node.test.ts | 4 +-
.../worker/src/app/handlers/package-app.ts | 5 +-
packages/worker/src/app/user-lookup.ts | 13 ++-
packages/worker/src/index.ts | 101 ++++++++++++------
packages/worker/src/mcp/generated-ui-api.ts | 1 +
.../worker/src/mcp/tools/open-generated-ui.ts | 11 +-
.../worker/src/mcp/tools/search-format.ts | 10 +-
.../package-invocations/http.workers.test.ts | 36 ++++---
.../worker/src/package-invocations/service.ts | 1 +
.../worker/src/package-retrievers/service.ts | 1 +
.../worker/src/package-runtime/package-app.ts | 1 +
.../src/package-runtime/package-service.ts | 1 +
.../src/package-runtime/package-workflows.ts | 1 +
.../src/package-runtime/realtime-session.ts | 1 +
.../public-route-hardening.workers.test.ts | 8 ++
18 files changed, 137 insertions(+), 70 deletions(-)
diff --git a/packages/shared/src/public-urls.ts b/packages/shared/src/public-urls.ts
index 5946eff4f0..50c60bbf35 100644
--- a/packages/shared/src/public-urls.ts
+++ b/packages/shared/src/public-urls.ts
@@ -8,9 +8,7 @@ export function buildPackageAppPath(input: {
restPath?: string | null
}) {
const restPath = input.restPath?.trim()
- const suffix = restPath
- ? `/${restPath.replace(/^\/+/, '')}`
- : ''
+ const suffix = restPath ? `/${restPath.replace(/^\/+/, '')}` : ''
return `${buildUsernamePathPrefix(input.username)}/packages/${encodeURIComponent(
input.kodyId.trim(),
)}${suffix}`
diff --git a/packages/worker/client/routes/account-remote-connectors.tsx b/packages/worker/client/routes/account-remote-connectors.tsx
index 67da1ad431..8e094362f8 100644
--- a/packages/worker/client/routes/account-remote-connectors.tsx
+++ b/packages/worker/client/routes/account-remote-connectors.tsx
@@ -293,7 +293,7 @@ export function AccountRemoteConnectorsRoute(handle: Handle) {
let status: AccountStatus = 'loading'
let saveState: 'idle' | 'saving' | 'deleting' = 'idle'
let email = ''
-let username = ''
+ let username = ''
let connectorUrlOrigin = ''
let connectors: Array = []
let editorState = createEmptyEditorState()
diff --git a/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts b/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
index d69571e280..1e1f195964 100644
--- a/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
+++ b/packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
@@ -104,8 +104,7 @@ test('remote connector settings API lists settings with plaintext secrets', asyn
id: 'connector-1',
kind: 'lights',
instanceId: 'default',
- connectorUrl:
- 'wss://example.com/@test-user/connectors/lights/default',
+ connectorUrl: 'wss://example.com/@test-user/connectors/lights/default',
enabled: true,
attached: true,
hasSharedSecret: true,
@@ -155,8 +154,7 @@ test('remote connector settings API passes submitted secret to save service', as
id: 'connector-1',
kind: 'lights',
instanceId: 'default',
- connectorUrl:
- 'wss://example.com/@test-user/connectors/lights/default',
+ connectorUrl: 'wss://example.com/@test-user/connectors/lights/default',
enabled: true,
attached: true,
hasSharedSecret: true,
diff --git a/packages/worker/src/app/handlers/package-app.node.test.ts b/packages/worker/src/app/handlers/package-app.node.test.ts
index 5db9ed964a..0d12b8716e 100644
--- a/packages/worker/src/app/handlers/package-app.node.test.ts
+++ b/packages/worker/src/app/handlers/package-app.node.test.ts
@@ -105,7 +105,9 @@ test('handlePackageAppRequest reports host setup failures with helpful responses
resetMocks()
const response = await handlePackageAppRequest(
- new Request('https://example.com/@test-user/packages/example/report?tab=errors'),
+ new Request(
+ 'https://example.com/@test-user/packages/example/report?tab=errors',
+ ),
{} as Env,
)
diff --git a/packages/worker/src/app/handlers/package-app.ts b/packages/worker/src/app/handlers/package-app.ts
index cb0be32a37..d9954e43cb 100644
--- a/packages/worker/src/app/handlers/package-app.ts
+++ b/packages/worker/src/app/handlers/package-app.ts
@@ -235,10 +235,7 @@ function createPackageAppErrorResponse(input: {
)
}
-export async function handlePackageAppRequest(
- request: Request,
- env: Env,
-) {
+export async function handlePackageAppRequest(request: Request, env: Env) {
const requestUrl = new URL(request.url)
const packagePath = parsePackageAppPath(requestUrl.pathname)
if (!packagePath) {
diff --git a/packages/worker/src/app/user-lookup.ts b/packages/worker/src/app/user-lookup.ts
index 75bd245b27..5ed414acb4 100644
--- a/packages/worker/src/app/user-lookup.ts
+++ b/packages/worker/src/app/user-lookup.ts
@@ -1,5 +1,4 @@
import { getUsernameValidationError } from '#app/username.ts'
-import { createDb, usersTable } from '#worker/db.ts'
import { createStableUserIdFromEmail } from '#worker/user-id.ts'
export type PublicUserIdentity = {
@@ -16,10 +15,14 @@ export async function findPublicUserIdentityByUsername(input: {
const username = input.username.trim()
if (getUsernameValidationError(username)) return null
- const db = createDb(input.db)
- const userRecord = await db.findOne(usersTable, {
- where: { username },
- })
+ const userRecord = await input.db
+ .prepare(
+ `SELECT id, username, email
+ FROM users
+ WHERE username = ?`,
+ )
+ .bind(username)
+ .first<{ id: number; username: string; email: string }>()
if (!userRecord) return null
return {
diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts
index 89048ce4be..464fffd4c7 100644
--- a/packages/worker/src/index.ts
+++ b/packages/worker/src/index.ts
@@ -68,6 +68,55 @@ export {
const claudeWidgetDomainSuffix = '.claudemcpcontent.com'
+function isNamespacedPackageInvocationEndpointPath(pathname: string) {
+ const parts = pathname.split('/').filter(Boolean)
+ return (
+ parts[0]?.startsWith('@') === true &&
+ parts[1] === 'api' &&
+ parts[2] === 'package-invocations'
+ )
+}
+
+function isNamespacedAppEndpointPath(pathname: string) {
+ const parts = pathname.split('/').filter(Boolean)
+ return (
+ parts[0]?.startsWith('@') === true &&
+ (parts[1] === 'packages' || parts[1] === 'connectors')
+ )
+}
+
+async function handleUserScopedConnectorRequest(request: Request, env: Env) {
+ const url = new URL(request.url)
+ const userScopedConnectorRoute = parseUserScopedConnectorRoutePath(
+ url.pathname,
+ )
+ if (!userScopedConnectorRoute) return null
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
+ return new Response('Not Found', { status: 404 })
+ }
+ const routeUser = await findPublicUserIdentityByUsername({
+ db: env.APP_DB,
+ username: userScopedConnectorRoute.username,
+ })
+ if (!routeUser) {
+ return new Response('Not Found', { status: 404 })
+ }
+ const sessionKey = userScopedConnectorSessionKey({
+ userId: routeUser.mcpUserId,
+ kind: userScopedConnectorRoute.kind,
+ instanceId: userScopedConnectorRoute.instanceId,
+ })
+ const stub = env.REMOTE_CONNECTOR_SESSION.get(
+ env.REMOTE_CONNECTOR_SESSION.idFromName(sessionKey),
+ )
+ const forwardUrl = new URL(request.url)
+ forwardUrl.pathname = userScopedConnectorRoute.rest || '/'
+ const forwardRequest = new Request(forwardUrl.toString(), request)
+ forwardRequest.headers.set('X-Kody-Connector-Session-Key', sessionKey)
+ forwardRequest.headers.set('X-Kody-Connector-User-Id', routeUser.mcpUserId)
+ return stub.fetch(forwardRequest)
+}
+
function isAllowedGeneratedUiOrigin(origin: string, requestOrigin: string) {
if (origin === requestOrigin) {
return true
@@ -194,37 +243,17 @@ const appHandler = withCors({
return handlePackageAppRequest(request, env)
}
- const userScopedConnectorRoute = parseUserScopedConnectorRoutePath(
- url.pathname,
+ const connectorResponse = await handleUserScopedConnectorRequest(
+ request,
+ env,
)
- if (userScopedConnectorRoute) {
- if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket') {
- return new Response('Not Found', { status: 404 })
- }
- const routeUser = await findPublicUserIdentityByUsername({
- db: env.APP_DB,
- username: userScopedConnectorRoute.username,
- })
- if (!routeUser) {
- return new Response('Not Found', { status: 404 })
- }
- const sessionKey = userScopedConnectorSessionKey({
- userId: routeUser.mcpUserId,
- kind: userScopedConnectorRoute.kind,
- instanceId: userScopedConnectorRoute.instanceId,
- })
- const stub = env.REMOTE_CONNECTOR_SESSION.get(
- env.REMOTE_CONNECTOR_SESSION.idFromName(sessionKey),
- )
- const forwardUrl = new URL(request.url)
- forwardUrl.pathname = userScopedConnectorRoute.rest || '/'
- const forwardRequest = new Request(forwardUrl.toString(), request)
- forwardRequest.headers.set('X-Kody-Connector-Session-Key', sessionKey)
- forwardRequest.headers.set(
- 'X-Kody-Connector-User-Id',
- routeUser.mcpUserId,
- )
- return stub.fetch(forwardRequest)
+ if (connectorResponse) return connectorResponse
+
+ if (
+ isNamespacedAppEndpointPath(url.pathname) ||
+ isNamespacedPackageInvocationEndpointPath(url.pathname)
+ ) {
+ return new Response('Not Found', { status: 404 })
}
if (url.pathname.startsWith('/connectors/')) {
@@ -318,12 +347,22 @@ function addOAuthDiscoveryCorsHeaders(
}
const workerHandler = {
- fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url)
if (isPackageInvocationApiRequest(url.pathname)) {
return handlePackageInvocationApiRequest(request, env, ctx)
}
+ const connectorResponse = await handleUserScopedConnectorRequest(
+ request,
+ env,
+ )
+ if (connectorResponse) return connectorResponse
+
+ if (isNamespacedPackageInvocationEndpointPath(url.pathname)) {
+ return new Response('Not Found', { status: 404 })
+ }
+
// OAuthProvider serves this URL first and defaults `resource` to the origin only.
// MCP clients must use `/mcp` as the resource (RFC 8707) to match our
// token audience; otherwise authorize stores origin but the token request sends
diff --git a/packages/worker/src/mcp/generated-ui-api.ts b/packages/worker/src/mcp/generated-ui-api.ts
index bf0eec2a6e..bacff8c574 100644
--- a/packages/worker/src/mcp/generated-ui-api.ts
+++ b/packages/worker/src/mcp/generated-ui-api.ts
@@ -157,6 +157,7 @@ function createGeneratedUiExecuteHandler(env: Env) {
user: {
userId: context.user.userId,
email: context.user.email,
+ username: undefined,
displayName: '',
},
storageContext: {
diff --git a/packages/worker/src/mcp/tools/open-generated-ui.ts b/packages/worker/src/mcp/tools/open-generated-ui.ts
index a76631a3a2..4ce9d910af 100644
--- a/packages/worker/src/mcp/tools/open-generated-ui.ts
+++ b/packages/worker/src/mcp/tools/open-generated-ui.ts
@@ -81,6 +81,13 @@ const inputSchema = z
path: ['code'],
})
+function requireSavedPackageAppUsername(username: string | null) {
+ if (!username) {
+ throw new Error('Username is required to open saved package apps.')
+ }
+ return username
+}
+
export async function registerOpenGeneratedUiTool(agent: McpRegistrationAgent) {
registerAppTool(
agent.server,
@@ -119,14 +126,14 @@ export async function registerOpenGeneratedUiTool(agent: McpRegistrationAgent) {
)
}
}
- const username = callerContext.user?.username
+ const username = callerContext.user?.username ?? null
if (savedPackage && !username) {
throw new Error('Username is required to open saved package apps.')
}
const hostedUrl = savedPackage
? buildPackageAppUrl({
origin: agent.requireDomain(),
- username,
+ username: requireSavedPackageAppUsername(username),
kodyId: savedPackage.kodyId,
})
: null
diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts
index 3406d1c2fa..d428fe9e11 100644
--- a/packages/worker/src/mcp/tools/search-format.ts
+++ b/packages/worker/src/mcp/tools/search-format.ts
@@ -406,11 +406,17 @@ export type SearchMatch =
type: 'retriever_result'
})
-function buildPackageHostedUrl(baseUrl: string, username: string, kodyId: string) {
+function buildPackageHostedUrl(
+ baseUrl: string,
+ username: string,
+ kodyId: string,
+) {
return buildPackageAppUrl({ origin: baseUrl, username, kodyId })
}
-function requireUsernameForHostedPackageUrl(username: string | null) {
+function requireUsernameForHostedPackageUrl(
+ username: string | null | undefined,
+) {
if (!username) {
throw new Error('Username is required to build hosted package app URLs.')
}
diff --git a/packages/worker/src/package-invocations/http.workers.test.ts b/packages/worker/src/package-invocations/http.workers.test.ts
index 8e00af19a1..0da6d87d05 100644
--- a/packages/worker/src/package-invocations/http.workers.test.ts
+++ b/packages/worker/src/package-invocations/http.workers.test.ts
@@ -67,16 +67,18 @@ async function createEnv(
async first>() {
if (query.includes('FROM users')) {
const username = String(params[0] ?? '')
- return (username === 'me'
- ? {
- id: 1,
- username: 'me',
- email: 'me@example.com',
- password_hash: 'hash',
- created_at: '2026-04-27T00:00:00.000Z',
- updated_at: '2026-04-27T00:00:00.000Z',
- }
- : null) as T | null
+ return (
+ username === 'my-user'
+ ? {
+ id: 1,
+ username: 'my-user',
+ email: 'me@example.com',
+ password_hash: 'hash',
+ created_at: '2026-04-27T00:00:00.000Z',
+ updated_at: '2026-04-27T00:00:00.000Z',
+ }
+ : null
+ ) as T | null
}
if (query.includes('FROM package_invocation_tokens')) {
const tokenHash = String(params[0] ?? '')
@@ -152,7 +154,7 @@ function createContext() {
test('isPackageInvocationApiRequest matches the external package invocation route', () => {
expect(
isPackageInvocationApiRequest(
- '/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ '/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
),
).toBe(true)
expect(isPackageInvocationApiRequest('/api/me')).toBe(false)
@@ -161,7 +163,7 @@ test('isPackageInvocationApiRequest matches the external package invocation rout
test('package invocation API returns 401 when bearer token is missing', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -188,7 +190,7 @@ test('package invocation API returns 401 when bearer token is missing', async ()
test('package invocation API returns 401 for invalid private tokens', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -215,7 +217,7 @@ test('package invocation API returns 401 for invalid private tokens', async () =
test('package invocation API fails closed when token touch loses revocation race', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -243,7 +245,7 @@ test('package invocation API fails closed when token touch loses revocation race
test('package invocation API fails closed when token scope JSON is malformed', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -275,7 +277,7 @@ test('package invocation API fails closed when token scope JSON is malformed', a
test('package invocation API validates the JSON body shape', async () => {
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
@@ -318,7 +320,7 @@ test('package invocation API invokes the package export with the scoped token co
const expectedUserId = await createStableUserIdFromEmail('me@example.com')
const response = await handlePackageInvocationApiRequest(
new Request(
- 'https://example.com/@me/api/package-invocations/discord-gateway/dispatch-message-created',
+ 'https://example.com/@my-user/api/package-invocations/discord-gateway/dispatch-message-created',
{
method: 'POST',
headers: {
diff --git a/packages/worker/src/package-invocations/service.ts b/packages/worker/src/package-invocations/service.ts
index 64abd56a47..fbe9efda4b 100644
--- a/packages/worker/src/package-invocations/service.ts
+++ b/packages/worker/src/package-invocations/service.ts
@@ -1041,6 +1041,7 @@ async function invokeSavedPackageModule(input: {
user: {
userId: input.actor.userId,
email: input.actor.email,
+ username: undefined,
displayName: input.actor.displayName,
},
storageContext: {
diff --git a/packages/worker/src/package-retrievers/service.ts b/packages/worker/src/package-retrievers/service.ts
index e9e57be891..3ad1bcd669 100644
--- a/packages/worker/src/package-retrievers/service.ts
+++ b/packages/worker/src/package-retrievers/service.ts
@@ -128,6 +128,7 @@ async function invokeRetriever(input: {
user: {
userId: input.userId,
email: '',
+ username: undefined,
displayName: '',
},
storageContext: {
diff --git a/packages/worker/src/package-runtime/package-app.ts b/packages/worker/src/package-runtime/package-app.ts
index 7f3c50af26..1519fa9034 100644
--- a/packages/worker/src/package-runtime/package-app.ts
+++ b/packages/worker/src/package-runtime/package-app.ts
@@ -628,6 +628,7 @@ export class PackageAppRuntimeBridge extends WorkerEntrypoint<
user: {
userId: this.ctx.props.userId,
email: this.ctx.props.email,
+ username: undefined,
displayName: this.ctx.props.displayName,
},
storageContext: {
diff --git a/packages/worker/src/package-runtime/package-service.ts b/packages/worker/src/package-runtime/package-service.ts
index 5ee870d053..e5007841c6 100644
--- a/packages/worker/src/package-runtime/package-service.ts
+++ b/packages/worker/src/package-runtime/package-service.ts
@@ -526,6 +526,7 @@ class PackageServiceInstanceBase extends DurableObject {
user: {
userId: binding.userId,
email: '',
+ username: undefined,
displayName: `package:${binding.packageId}`,
},
storageContext: {
diff --git a/packages/worker/src/package-runtime/package-workflows.ts b/packages/worker/src/package-runtime/package-workflows.ts
index 0105cdd850..e9e8b3451a 100644
--- a/packages/worker/src/package-runtime/package-workflows.ts
+++ b/packages/worker/src/package-runtime/package-workflows.ts
@@ -1041,6 +1041,7 @@ export class DynamicCallableWorkflowBase extends WorkflowEntrypoint<
user: {
userId: payload.userId,
email: '',
+ username: undefined,
displayName: `workflow:${payload.workflowName}`,
},
remoteConnectors,
diff --git a/packages/worker/src/package-runtime/realtime-session.ts b/packages/worker/src/package-runtime/realtime-session.ts
index 57211b4987..c2f13bc683 100644
--- a/packages/worker/src/package-runtime/realtime-session.ts
+++ b/packages/worker/src/package-runtime/realtime-session.ts
@@ -314,6 +314,7 @@ async function resolvePackageAppWorker(input: {
user: {
userId: input.binding.userId,
email: '',
+ username: undefined,
displayName: `package:${input.binding.packageId}`,
},
storageContext: {
diff --git a/packages/worker/src/security/public-route-hardening.workers.test.ts b/packages/worker/src/security/public-route-hardening.workers.test.ts
index bf1a8b181f..22ec851347 100644
--- a/packages/worker/src/security/public-route-hardening.workers.test.ts
+++ b/packages/worker/src/security/public-route-hardening.workers.test.ts
@@ -17,6 +17,14 @@ async function workerFetch(request: Request): Promise {
}
test('user-scoped connector entrypoints reject unauthenticated HTTP access while allowing WebSocket upgrades', async () => {
+ await env.APP_DB.prepare(
+ `CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ username TEXT NOT NULL UNIQUE,
+ email TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL
+ )`,
+ ).run()
await env.APP_DB.prepare(
`INSERT OR IGNORE INTO users (username, email, password_hash)
VALUES ('connector-user', 'connector-user@example.com', 'hash')`,
From de2284e6c3efa242f26d6b961bf91d974f309d54 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 19:05:19 +0000
Subject: [PATCH 3/7] Update OAuth username session test
Co-authored-by: Kent C. Dodds
---
.../worker/src/oauth-handlers.workers.test.ts | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/packages/worker/src/oauth-handlers.workers.test.ts b/packages/worker/src/oauth-handlers.workers.test.ts
index c599128e27..cac5ac8dbe 100644
--- a/packages/worker/src/oauth-handlers.workers.test.ts
+++ b/packages/worker/src/oauth-handlers.workers.test.ts
@@ -61,12 +61,24 @@ async function createDatabase(password: string) {
return {
async all() {
return {
- results: [{ id: 1, password_hash: passwordHash }],
+ results: [
+ {
+ id: 1,
+ username: 'test-user',
+ email: 'user@example.com',
+ password_hash: passwordHash,
+ },
+ ],
meta: { changes: 0, last_row_id: 0 },
}
},
async first() {
- return { id: 1, password_hash: passwordHash }
+ return {
+ id: 1,
+ username: 'test-user',
+ email: 'user@example.com',
+ password_hash: passwordHash,
+ }
},
async run() {
return { meta: { changes: 1, last_row_id: 1 } }
@@ -243,7 +255,7 @@ test('authorize allows approval with an existing session', async () => {
{ decision: 'approve' },
{ Accept: 'application/json', Cookie: cookie },
),
- createEnv(helpers),
+ createEnv(helpers, await createDatabase('password123')),
)
expect(response.status).toBe(200)
From 7986542ab0f3862ba11c862d5217136dc69152bd Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 19:05:28 +0000
Subject: [PATCH 4/7] Add username field to job caller context
Co-authored-by: Kent C. Dodds
---
packages/worker/src/jobs/service.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/worker/src/jobs/service.ts b/packages/worker/src/jobs/service.ts
index cf572060df..2709d1d955 100644
--- a/packages/worker/src/jobs/service.ts
+++ b/packages/worker/src/jobs/service.ts
@@ -662,6 +662,7 @@ function createPackageJobCallerContext(input: {
user: {
userId: input.userId,
email: '',
+ username: undefined,
displayName: `package:${input.packageId}`,
},
storageContext: {
From 6515aafebf010721e0c7a8115d29d47e3ba31e5a Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 19:31:28 +0000
Subject: [PATCH 5/7] Resolve public URL usernames for MCP tools
Co-authored-by: Kent C. Dodds
---
packages/shared/src/public-urls.ts | 11 +++++
packages/worker/src/app/user-lookup.ts | 23 ++++++++++
.../mcp/tools/open-generated-ui.node.test.ts | 44 +++++++++++++++++--
.../worker/src/mcp/tools/open-generated-ui.ts | 24 +++++-----
.../src/mcp/tools/search-format.node.test.ts | 26 +++++++++++
.../worker/src/mcp/tools/search-format.ts | 20 ++-------
packages/worker/src/mcp/tools/search.ts | 26 ++++++-----
packages/worker/src/oauth-handlers.ts | 9 ++++
8 files changed, 138 insertions(+), 45 deletions(-)
diff --git a/packages/shared/src/public-urls.ts b/packages/shared/src/public-urls.ts
index 50c60bbf35..129b79f43c 100644
--- a/packages/shared/src/public-urls.ts
+++ b/packages/shared/src/public-urls.ts
@@ -22,3 +22,14 @@ export function buildPackageAppUrl(input: {
}) {
return `${input.origin.trim().replace(/\/+$/, '')}${buildPackageAppPath(input)}`
}
+
+export function requireUsernameForPublicUrl(
+ username: string | null | undefined,
+) {
+ if (!username) {
+ throw new Error(
+ 'Username is required to build username-scoped public URLs.',
+ )
+ }
+ return username
+}
diff --git a/packages/worker/src/app/user-lookup.ts b/packages/worker/src/app/user-lookup.ts
index 5ed414acb4..90260d95bc 100644
--- a/packages/worker/src/app/user-lookup.ts
+++ b/packages/worker/src/app/user-lookup.ts
@@ -32,3 +32,26 @@ export async function findPublicUserIdentityByUsername(input: {
mcpUserId: await createStableUserIdFromEmail(userRecord.email),
}
}
+
+export async function resolvePublicUsername(input: {
+ db: D1Database
+ username?: string | null
+ email?: string | null
+}): Promise {
+ const username = input.username?.trim()
+ if (username) return username
+
+ const email = input.email?.trim().toLowerCase()
+ if (!email) return null
+
+ const userRecord = await input.db
+ .prepare(
+ `SELECT username
+ FROM users
+ WHERE email = ?`,
+ )
+ .bind(email)
+ .first<{ username: string }>()
+
+ return userRecord?.username ?? null
+}
diff --git a/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts b/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts
index 982544c310..85b91e2768 100644
--- a/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts
+++ b/packages/worker/src/mcp/tools/open-generated-ui.node.test.ts
@@ -33,20 +33,23 @@ vi.mock('#mcp/generated-ui-app-session.ts', () => ({
const { registerOpenGeneratedUiTool } = await import('./open-generated-ui.ts')
-async function getOpenGeneratedUiHandler() {
+async function getOpenGeneratedUiHandler(options?: {
+ username?: string
+ appDb?: D1Database
+}) {
mockModule.registerAppTool.mockClear()
await registerOpenGeneratedUiTool({
server: {} as never,
getEnv: vi.fn(() => ({
- APP_DB: {} as D1Database,
+ APP_DB: options?.appDb ?? ({} as D1Database),
})),
getCallerContext: vi.fn(() => ({
baseUrl: 'https://example.com',
user: {
userId: 'user-123',
email: 'user@example.com',
- username: 'test-user',
+ username: options?.username,
displayName: 'test-user',
},
})),
@@ -65,7 +68,7 @@ async function getOpenGeneratedUiHandler() {
}
test('open_generated_ui reopens saved package apps by kody_id', async () => {
- const handler = await getOpenGeneratedUiHandler()
+ const handler = await getOpenGeneratedUiHandler({ username: 'test-user' })
mockModule.getSavedPackageByKodyId.mockResolvedValueOnce({
id: 'package-123',
userId: 'user-123',
@@ -96,3 +99,36 @@ test('open_generated_ui reopens saved package apps by kody_id', async () => {
hostedUrl: 'https://example.com/@test-user/packages/observed-package',
})
})
+
+test('open_generated_ui resolves username from email for older caller contexts', async () => {
+ const handler = await getOpenGeneratedUiHandler({
+ appDb: {
+ prepare: () => ({
+ bind: () => ({
+ first: async () => ({ username: 'resolved-user' }),
+ }),
+ }),
+ } as unknown as D1Database,
+ })
+ mockModule.getSavedPackageByKodyId.mockResolvedValueOnce({
+ id: 'package-123',
+ userId: 'user-123',
+ name: '@kody/observed',
+ kodyId: 'observed-package',
+ description: 'Observed package app',
+ tags: ['ui'],
+ searchText: null,
+ sourceId: 'source-123',
+ hasApp: true,
+ createdAt: '2026-04-21T00:00:00.000Z',
+ updatedAt: '2026-04-21T00:00:00.000Z',
+ })
+
+ const response = await handler({
+ kody_id: 'observed-package',
+ })
+
+ expect(response.structuredContent).toMatchObject({
+ hostedUrl: 'https://example.com/@resolved-user/packages/observed-package',
+ })
+})
diff --git a/packages/worker/src/mcp/tools/open-generated-ui.ts b/packages/worker/src/mcp/tools/open-generated-ui.ts
index 4ce9d910af..230127bec3 100644
--- a/packages/worker/src/mcp/tools/open-generated-ui.ts
+++ b/packages/worker/src/mcp/tools/open-generated-ui.ts
@@ -5,7 +5,11 @@ import { generatedUiRuntimeResourceUri } from '#mcp/apps/generated-ui-runtime-ht
import { createGeneratedUiAppSession } from '#mcp/generated-ui-app-session.ts'
import { getSavedPackageByKodyId } from '#worker/package-registry/repo.ts'
import { type McpRegistrationAgent } from '#mcp/mcp-registration-agent.ts'
-import { buildPackageAppUrl } from '@kody-internal/shared/public-urls.ts'
+import {
+ buildPackageAppUrl,
+ requireUsernameForPublicUrl,
+} from '@kody-internal/shared/public-urls.ts'
+import { resolvePublicUsername } from '#app/user-lookup.ts'
import {
conversationIdInputField,
memoryContextInputField,
@@ -81,13 +85,6 @@ const inputSchema = z
path: ['code'],
})
-function requireSavedPackageAppUsername(username: string | null) {
- if (!username) {
- throw new Error('Username is required to open saved package apps.')
- }
- return username
-}
-
export async function registerOpenGeneratedUiTool(agent: McpRegistrationAgent) {
registerAppTool(
agent.server,
@@ -126,14 +123,15 @@ export async function registerOpenGeneratedUiTool(agent: McpRegistrationAgent) {
)
}
}
- const username = callerContext.user?.username ?? null
- if (savedPackage && !username) {
- throw new Error('Username is required to open saved package apps.')
- }
+ const username = await resolvePublicUsername({
+ db: agent.getEnv().APP_DB,
+ username: callerContext.user?.username ?? null,
+ email: callerContext.user?.email ?? null,
+ })
const hostedUrl = savedPackage
? buildPackageAppUrl({
origin: agent.requireDomain(),
- username: requireSavedPackageAppUsername(username),
+ username: requireUsernameForPublicUrl(username),
kodyId: savedPackage.kodyId,
})
: null
diff --git a/packages/worker/src/mcp/tools/search-format.node.test.ts b/packages/worker/src/mcp/tools/search-format.node.test.ts
index 32566e5e05..b9763d7a17 100644
--- a/packages/worker/src/mcp/tools/search-format.node.test.ts
+++ b/packages/worker/src/mcp/tools/search-format.node.test.ts
@@ -575,6 +575,32 @@ test('package search formatting keeps runnable package actions in structured out
expect(packageMatch?.nextStep).toEqual(expect.any(String))
})
+test('package search formatting omits hosted URLs when username is unavailable', () => {
+ const [packageMatch] = toSlimStructuredMatches({
+ baseUrl: 'http://localhost',
+ matches: [
+ {
+ type: 'package',
+ packageId: 'package-123',
+ kodyId: 'spotify-playback',
+ name: '@kody/spotify-playback',
+ title: '@kody/spotify-playback',
+ description: 'Saved package for Spotify playback controls.',
+ tags: ['spotify', 'playback'],
+ hasApp: true,
+ readmeSnippet: null,
+ },
+ ],
+ })
+
+ expect(packageMatch).toMatchObject({
+ type: 'package',
+ hasApp: true,
+ hostedUrl: null,
+ openGeneratedUiUsage: 'open_generated_ui({ kody_id: "spotify-playback" })',
+ })
+})
+
test('package search formatting surfaces matched action recipes compactly', () => {
const markdown = formatSearchMarkdown({
matches: [
diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts
index d428fe9e11..77b6b377aa 100644
--- a/packages/worker/src/mcp/tools/search-format.ts
+++ b/packages/worker/src/mcp/tools/search-format.ts
@@ -414,15 +414,6 @@ function buildPackageHostedUrl(
return buildPackageAppUrl({ origin: baseUrl, username, kodyId })
}
-function requireUsernameForHostedPackageUrl(
- username: string | null | undefined,
-) {
- if (!username) {
- throw new Error('Username is required to build hosted package app URLs.')
- }
- return username
-}
-
function buildEntityRef(id: string, type: SearchEntityType) {
return `${id}:${type}`
}
@@ -697,13 +688,10 @@ export function toSlimStructuredMatches(input: {
openGeneratedUiUsage,
tags: match.tags,
hasApp: match.hasApp,
- hostedUrl: match.hasApp
- ? buildPackageHostedUrl(
- input.baseUrl,
- requireUsernameForHostedPackageUrl(input.username),
- match.kodyId,
- )
- : null,
+ hostedUrl:
+ match.hasApp && input.username
+ ? buildPackageHostedUrl(input.baseUrl, input.username, match.kodyId)
+ : null,
readmeSnippet: match.readmeSnippet
? {
path: match.readmeSnippet.path,
diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts
index 547bb5e849..152719af6c 100644
--- a/packages/worker/src/mcp/tools/search.ts
+++ b/packages/worker/src/mcp/tools/search.ts
@@ -46,9 +46,13 @@ import {
type RemoteConnectorStatus,
} from '#worker/remote-connector/status.ts'
import { type McpCallerContext } from '@kody-internal/shared/chat.ts'
-import { buildPackageAppUrl } from '@kody-internal/shared/public-urls.ts'
+import {
+ buildPackageAppUrl,
+ requireUsernameForPublicUrl,
+} from '@kody-internal/shared/public-urls.ts'
import { normalizeRemoteConnectorRefs } from '@kody-internal/shared/remote-connectors.ts'
import { type PackageRetrieverSurfaceResult } from '#worker/package-retrievers/types.ts'
+import { resolvePublicUsername } from '#app/user-lookup.ts'
import {
callerContextFields,
errorFields,
@@ -171,13 +175,6 @@ type SearchUnifiedResult = {
guidance?: string
}
-function requireUsernameForHostedPackageUrl(username: string | null) {
- if (!username) {
- throw new Error('Username is required to build hosted package app URLs.')
- }
- return username
-}
-
function flattenReferencedTypeFields(
referencedTypes:
| ReadonlyArray<{ name: string; definition: string }>
@@ -1637,6 +1634,7 @@ async function resolveEntityDetail(input: {
agent: McpRegistrationAgent
callerContext: ReturnType
userId: string | null
+ username: string | null
entity: string
searchRows: SearchRowsAndRegistry
}) {
@@ -1684,9 +1682,7 @@ async function resolveEntityDetail(input: {
hostedUrl: record.hasApp
? buildPackageAppUrl({
origin: input.callerContext.baseUrl,
- username: requireUsernameForHostedPackageUrl(
- input.callerContext.user?.username ?? null,
- ),
+ username: requireUsernameForPublicUrl(input.username),
kodyId: record.kodyId,
})
: null,
@@ -1797,6 +1793,11 @@ export async function registerSearchTool(agent: McpRegistrationAgent) {
const callerContext = agent.getCallerContext()
const { baseUrl, hasUser } = callerContextFields(callerContext)
const userId = callerContext.user?.userId ?? null
+ const username = await resolvePublicUsername({
+ db: agent.getEnv().APP_DB,
+ username: callerContext.user?.username ?? null,
+ email: callerContext.user?.email ?? null,
+ })
if (!args.query && !args.entity) {
const timing = finishToolTiming(timingStart)
logMcpEvent({
@@ -1891,6 +1892,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) {
agent,
callerContext,
userId,
+ username,
entity: args.entity,
searchRows,
}),
@@ -2088,7 +2090,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) {
matches: toSlimStructuredMatches({
matches: trimmedPayload.matches,
baseUrl,
- username: callerContext.user?.username ?? null,
+ username,
}),
}
const timing = finishToolTiming(timingStart)
diff --git a/packages/worker/src/oauth-handlers.ts b/packages/worker/src/oauth-handlers.ts
index e043d1728a..58b7dc044f 100644
--- a/packages/worker/src/oauth-handlers.ts
+++ b/packages/worker/src/oauth-handlers.ts
@@ -662,6 +662,15 @@ export async function handleAuthorizeRequest(
where: { email: sessionEmail },
})
if (!userRecord) {
+ void logAuditEvent({
+ category: 'oauth',
+ action: 'authorize',
+ result: 'failure',
+ email: sessionEmail,
+ ip: requestIp,
+ clientId: authRequest.clientId,
+ reason: 'session_user_not_found',
+ })
return respondAuthorizeError(request, 'Signed-in user not found.', 401)
}
approvedEmail = sessionEmail
From d3621d0fa8f037ac1b53a767e29473fbec9c4817 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 19:43:24 +0000
Subject: [PATCH 6/7] Harden username resolution for public URLs
Co-authored-by: Kent C. Dodds
---
packages/worker/src/app/user-lookup.ts | 9 ++++--
packages/worker/src/mcp/tools/search.ts | 20 ++++++-------
packages/worker/src/oauth-handlers.ts | 37 +++++++++++++++++++++++--
3 files changed, 50 insertions(+), 16 deletions(-)
diff --git a/packages/worker/src/app/user-lookup.ts b/packages/worker/src/app/user-lookup.ts
index 90260d95bc..abf70c7145 100644
--- a/packages/worker/src/app/user-lookup.ts
+++ b/packages/worker/src/app/user-lookup.ts
@@ -39,7 +39,7 @@ export async function resolvePublicUsername(input: {
email?: string | null
}): Promise {
const username = input.username?.trim()
- if (username) return username
+ if (username && !getUsernameValidationError(username)) return username
const email = input.email?.trim().toLowerCase()
if (!email) return null
@@ -51,7 +51,10 @@ export async function resolvePublicUsername(input: {
WHERE email = ?`,
)
.bind(email)
- .first<{ username: string }>()
+ .first<{ username: string | null }>()
- return userRecord?.username ?? null
+ const resolvedUsername = userRecord?.username?.trim()
+ return resolvedUsername && !getUsernameValidationError(resolvedUsername)
+ ? resolvedUsername
+ : null
}
diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts
index 152719af6c..f662bbd3ba 100644
--- a/packages/worker/src/mcp/tools/search.ts
+++ b/packages/worker/src/mcp/tools/search.ts
@@ -46,10 +46,7 @@ import {
type RemoteConnectorStatus,
} from '#worker/remote-connector/status.ts'
import { type McpCallerContext } from '@kody-internal/shared/chat.ts'
-import {
- buildPackageAppUrl,
- requireUsernameForPublicUrl,
-} from '@kody-internal/shared/public-urls.ts'
+import { buildPackageAppUrl } from '@kody-internal/shared/public-urls.ts'
import { normalizeRemoteConnectorRefs } from '@kody-internal/shared/remote-connectors.ts'
import { type PackageRetrieverSurfaceResult } from '#worker/package-retrievers/types.ts'
import { resolvePublicUsername } from '#app/user-lookup.ts'
@@ -1679,13 +1676,14 @@ async function resolveEntityDetail(input: {
record,
manifest: loaded.manifest,
files: loaded.files,
- hostedUrl: record.hasApp
- ? buildPackageAppUrl({
- origin: input.callerContext.baseUrl,
- username: requireUsernameForPublicUrl(input.username),
- kodyId: record.kodyId,
- })
- : null,
+ hostedUrl:
+ record.hasApp && input.username
+ ? buildPackageAppUrl({
+ origin: input.callerContext.baseUrl,
+ username: input.username,
+ kodyId: record.kodyId,
+ })
+ : null,
}
}
diff --git a/packages/worker/src/oauth-handlers.ts b/packages/worker/src/oauth-handlers.ts
index 58b7dc044f..3f2af878ad 100644
--- a/packages/worker/src/oauth-handlers.ts
+++ b/packages/worker/src/oauth-handlers.ts
@@ -17,6 +17,7 @@ import { createDb, usersTable } from './db.ts'
import { wantsJson } from './utils.ts'
import { verifyPassword } from '@kody-internal/shared/password-hash.ts'
import { invalidClientIdMismatchMessage } from '@kody-internal/shared/oauth-messages.ts'
+import { getUsernameValidationError } from '#app/username.ts'
export const oauthPaths = {
authorize: '/oauth/authorize',
@@ -44,6 +45,12 @@ type OAuthContext = ExecutionContext & {
props?: OAuthProps
}
+function getValidOAuthUsername(value: unknown) {
+ return typeof value === 'string' && !getUsernameValidationError(value.trim())
+ ? value.trim()
+ : null
+}
+
type OAuthClientResetVerification = {
clientId: string
reason: 'invalid-client-id-mismatch'
@@ -654,8 +661,21 @@ export async function handleAuthorizeRequest(
})
return respondAuthorizeError(request, 'Invalid email or password.')
}
+ const username = getValidOAuthUsername(userRecord.username)
+ if (!username) {
+ void logAuditEvent({
+ category: 'oauth',
+ action: 'authorize',
+ result: 'failure',
+ email: normalizedEmail,
+ ip: requestIp,
+ clientId: authRequest.clientId,
+ reason: 'username_missing',
+ })
+ return respondAuthorizeError(request, 'Username is required.', 401)
+ }
approvedEmail = normalizedEmail
- approvedUsername = userRecord.username
+ approvedUsername = username
} else if (sessionEmail) {
const db = createDb(env.APP_DB)
const userRecord = await db.findOne(usersTable, {
@@ -673,8 +693,21 @@ export async function handleAuthorizeRequest(
})
return respondAuthorizeError(request, 'Signed-in user not found.', 401)
}
+ const username = getValidOAuthUsername(userRecord.username)
+ if (!username) {
+ void logAuditEvent({
+ category: 'oauth',
+ action: 'authorize',
+ result: 'failure',
+ email: sessionEmail,
+ ip: requestIp,
+ clientId: authRequest.clientId,
+ reason: 'username_missing',
+ })
+ return respondAuthorizeError(request, 'Username is required.', 401)
+ }
approvedEmail = sessionEmail
- approvedUsername = userRecord.username
+ approvedUsername = username
}
const resolvedScopes = resolveScopes(authRequest.scope)
From b1190f07eb0c7bdae840be8be40eecf2e66400ba Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Tue, 12 May 2026 19:51:57 +0000
Subject: [PATCH 7/7] Tighten search and connector routing
Co-authored-by: Kent C. Dodds
---
packages/worker/src/index.ts | 6 ------
packages/worker/src/mcp/tools/search.ts | 11 ++++++-----
2 files changed, 6 insertions(+), 11 deletions(-)
diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts
index 464fffd4c7..63b0884002 100644
--- a/packages/worker/src/index.ts
+++ b/packages/worker/src/index.ts
@@ -243,12 +243,6 @@ const appHandler = withCors({
return handlePackageAppRequest(request, env)
}
- const connectorResponse = await handleUserScopedConnectorRequest(
- request,
- env,
- )
- if (connectorResponse) return connectorResponse
-
if (
isNamespacedAppEndpointPath(url.pathname) ||
isNamespacedPackageInvocationEndpointPath(url.pathname)
diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts
index f662bbd3ba..b2314bedd6 100644
--- a/packages/worker/src/mcp/tools/search.ts
+++ b/packages/worker/src/mcp/tools/search.ts
@@ -1791,11 +1791,6 @@ export async function registerSearchTool(agent: McpRegistrationAgent) {
const callerContext = agent.getCallerContext()
const { baseUrl, hasUser } = callerContextFields(callerContext)
const userId = callerContext.user?.userId ?? null
- const username = await resolvePublicUsername({
- db: agent.getEnv().APP_DB,
- username: callerContext.user?.username ?? null,
- email: callerContext.user?.email ?? null,
- })
if (!args.query && !args.entity) {
const timing = finishToolTiming(timingStart)
logMcpEvent({
@@ -1833,9 +1828,15 @@ export async function registerSearchTool(agent: McpRegistrationAgent) {
const maxResponseSize = args.maxResponseSize ?? defaultMaxResponseSize
let warnings: Array = []
let remoteConnectorDownStatuses: Array = []
+ let username: string | null = null
const searchSpan = async () => {
const query = args.query?.trim() ?? ''
+ username = await resolvePublicUsername({
+ db: agent.getEnv().APP_DB,
+ username: callerContext.user?.username ?? null,
+ email: callerContext.user?.email ?? null,
+ })
const retrieverRunPromise =
userId && query
? (async () => {