diff --git a/packages/core/src/claustrum.ts b/packages/core/src/claustrum.ts index 58a991d9..07512bcc 100644 --- a/packages/core/src/claustrum.ts +++ b/packages/core/src/claustrum.ts @@ -408,14 +408,16 @@ function isValidCustodyCredentialId(value: unknown): value is string { // NOT enumerate or constrain it: enumerating the kinds would reject live // credentials the moment a new one ships. The third-and-later segments are // the label, which is never consulted here — the label is the lookup key -// elsewhere, not an authorization check. +// elsewhere, not an authorization check. Empty segments are invalid because +// the co-tenant parser rejects the whole manifest for a malformed row, while +// ours can otherwise isolate it to the row that contains it. function isScopedCustodyCredentialId( value: unknown, provider: string, ): value is string { if (typeof value !== 'string' || value.length === 0) return false const segments = value.split(':') - return segments[1] === provider + return segments[1] === provider && segments.every((segment) => segment !== '') } function legacyOrUnresolved( diff --git a/packages/core/src/tests/claustrum.test.ts b/packages/core/src/tests/claustrum.test.ts index 2b2f27f8..b2a22006 100644 --- a/packages/core/src/tests/claustrum.test.ts +++ b/packages/core/src/tests/claustrum.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from 'bun:test' -import { basename, win32 } from 'node:path' +import { afterEach, describe, expect, test } from 'bun:test' +import { exists, mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, join, win32 } from 'node:path' import { __deriveCustodyManifestStaleLockPrefix, @@ -8,8 +10,19 @@ import { custodyCredentialIdFromResolution, readCustodyHandles, resolveCustodyHandle, + writeCustodyHandleManifestEntry, } from '../claustrum.ts' +const temporaryDirectories: string[] = [] + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ) +}) + describe('custody manifest stale-lock prefix', () => { test('derives prefixes from POSIX and Windows path basenames', () => { expect( @@ -311,4 +324,67 @@ describe('readCustodyHandles provider scope', () => { expect(parsed.accounts[0]?.credentialId).toBe('antigravity:google') expect(parsed.accounts[0]?.label).toBe('main') }) + + test('resolves an `apikey:deepseek:main` id inside a deepseek block', () => { + const doc = makeManifest('deepseek', 'deepseek-auth') + doc.providers[0]!.accounts.push({ + label: 'main', + handle: anthropicHandle, + credential_id: 'apikey:deepseek:main', + }) + + const parsed = readCustodyHandles(doc, 'deepseek', 'deepseek-auth') + expect(parsed.corruptLabels).toEqual(new Set()) + expect(parsed.accounts).toHaveLength(1) + expect(parsed.accounts[0]?.credentialId).toBe('apikey:deepseek:main') + expect(parsed.accounts[0]?.label).toBe('main') + }) + + test('rejects credential ids with any empty colon-separated segment', () => { + const parseMalformed = (credentialId: string, label: string) => { + const doc = makeManifest('anthropic', 'anthropic-auth') + doc.providers[0]!.accounts.push({ + label, + handle: anthropicHandle, + credential_id: credentialId, + }) + + const parsed = readCustodyHandles(doc, 'anthropic', 'anthropic-auth') + return parsed + } + + expect(parseMalformed(':anthropic:x', 'empty-kind').corruptLabels).toEqual( + new Set(['empty-kind']), + ) + expect(parseMalformed('oauth::x', 'empty-provider').corruptLabels).toEqual( + new Set(['empty-provider']), + ) + expect( + parseMalformed('oauth:anthropic:', 'empty-label').corruptLabels, + ).toEqual(new Set(['empty-label'])) + expect( + parseMalformed('oauth:anthropic::y', 'empty-middle-label').corruptLabels, + ).toEqual(new Set(['empty-middle-label'])) + expect(parseMalformed('', 'empty-id').corruptLabels).toEqual( + new Set(['empty-id']), + ) + }) + + test('writer refuses empty-segment credential ids before they reach disk', async () => { + const directory = await mkdtemp(join(tmpdir(), 'claustrum-manifest-')) + temporaryDirectories.push(directory) + const path = join(directory, 'handles.json') + + await expect( + writeCustodyHandleManifestEntry({ + path, + entry: { + label: 'main', + handle: anthropicHandle, + credentialId: 'oauth:anthropic:', + }, + }), + ).resolves.toEqual({ status: 'refused', reason: 'invalid entry' }) + expect(await exists(path)).toBe(false) + }) })