fix(rpc): reject a blank GitHub owner or repo

normalizeTaskProviderIdentity treats a blank owner or repo as no identity at
all, but the schema accepted '' and whitespace-only, so the two disagreed about
the same payload. Refined rather than trimmed: trimming would rewrite the
parsed value and change what the handler receives.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-12 14:06:57 -04:00
parent a500162f12
commit cf4f77f275
2 changed files with 41 additions and 2 deletions
@@ -108,3 +108,34 @@ describe('task provider identity RPC validation', () => {
).toBe(false)
})
})
describe('github identity blank fields', () => {
// The normalizer treats a blank owner or repo as no identity, so the schema must agree.
it.each(['', ' ', '\t'])('rejects a blank owner %j', (owner) => {
expect(
TaskProviderIdentity.safeParse({ provider: 'github', owner, repo: 'orca' }).success
).toBe(false)
})
it.each(['', ' '])('rejects a blank repo %j', (repo) => {
expect(
TaskProviderIdentity.safeParse({ provider: 'github', owner: 'stablyai', repo }).success
).toBe(false)
})
it('still accepts a populated identity', () => {
expect(
TaskProviderIdentity.safeParse({ provider: 'github', owner: 'stablyai', repo: 'orca' })
.success
).toBe(true)
})
it('leaves the parsed value untrimmed, so no wire bytes change', () => {
const parsed = TaskProviderIdentity.safeParse({
provider: 'github',
owner: ' stablyai ',
repo: 'orca'
})
expect(parsed.success && parsed.data?.owner).toBe(' stablyai ')
})
})
+10 -2
View File
@@ -57,13 +57,21 @@ export const OptionalNullablePlainString = z
.pipe(z.union([z.string(), z.null(), z.undefined()]))
.optional()
// A GitHub identity is only usable with both fields present and non-blank.
const GithubIdentityField = z.string().refine((value) => value.trim().length > 0, {
message: 'Required'
})
export const TaskProviderIdentity = z
.discriminatedUnion('provider', [
z
.object({
provider: z.literal('github'),
owner: z.string(),
repo: z.string(),
// Why refine, not .trim(): normalizeTaskProviderIdentity treats a blank owner or repo as
// no identity at all, so blank must be rejected here — but trimming would rewrite the
// parsed value and change what the handler receives.
owner: GithubIdentityField,
repo: GithubIdentityField,
host: z.string().optional()
})
.passthrough(),