hardening: isolate third-party paths and clean external-build metadata (#311)
* hardening: isolate third-party paths and clean external-build metadata * fix: restore external feedback flow and make privacy check portable
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
import { logForDebugging } from './debug.js'
|
||||
import { parseJSONL } from './json.js'
|
||||
import { logError } from './log.js'
|
||||
import { getAPIProvider } from './model/providers.js'
|
||||
import {
|
||||
getCanonicalName,
|
||||
getMainLoopModel,
|
||||
@@ -75,11 +76,13 @@ export function getAttributionTexts(): AttributionTexts {
|
||||
: 'Claude Opus 4.6'
|
||||
const defaultAttribution =
|
||||
'🤖 Generated with [OpenClaude](https://github.com/Gitlawb/openclaude)'
|
||||
const coAuthorDomain =
|
||||
getAPIProvider() === 'firstParty' ? 'anthropic.com' : 'openclaude.dev'
|
||||
const defaultCommit = isEnvTruthy(
|
||||
process.env.OPENCLAUDE_DISABLE_CO_AUTHORED_BY,
|
||||
)
|
||||
? ''
|
||||
: `Co-Authored-By: ${modelName} <noreply@anthropic.com>`
|
||||
: `Co-Authored-By: ${modelName} <noreply@${coAuthorDomain}>`
|
||||
|
||||
const settings = getInitialSettings()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
handleOAuth401Error,
|
||||
isClaudeAISubscriber,
|
||||
} from './auth.js'
|
||||
import { getAPIProvider } from './model/providers.js'
|
||||
import { getClaudeCodeUserAgent } from './userAgent.js'
|
||||
import { getWorkload } from './workloadContext.js'
|
||||
|
||||
@@ -54,7 +55,11 @@ export function getMCPUserAgent(): string {
|
||||
// operators match in robots.txt); the claude-code suffix lets them distinguish
|
||||
// local CLI traffic from claude.ai server-side fetches.
|
||||
export function getWebFetchUserAgent(): string {
|
||||
return `Claude-User (${getClaudeCodeUserAgent()}; +https://support.anthropic.com/)`
|
||||
const supportUrl =
|
||||
getAPIProvider() === 'firstParty'
|
||||
? 'https://support.anthropic.com/'
|
||||
: 'https://github.com/Gitlawb/openclaude'
|
||||
return `Claude-User (${getClaudeCodeUserAgent()}; +${supportUrl})`
|
||||
}
|
||||
|
||||
export type AuthHeaders = {
|
||||
|
||||
File diff suppressed because one or more lines are too long
90
src/utils/user.test.ts
Normal file
90
src/utils/user.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
async function importFreshUserModule() {
|
||||
return import(`./user.ts?ts=${Date.now()}-${Math.random()}`)
|
||||
}
|
||||
|
||||
function installCommonMocks(options?: {
|
||||
oauthEmail?: string
|
||||
gitEmail?: string
|
||||
}) {
|
||||
mock.module('../bootstrap/state.js', () => ({
|
||||
getSessionId: () => 'session-test',
|
||||
}))
|
||||
|
||||
mock.module('./auth.js', () => ({
|
||||
getOauthAccountInfo: () =>
|
||||
options?.oauthEmail
|
||||
? {
|
||||
emailAddress: options.oauthEmail,
|
||||
organizationUuid: 'org-test',
|
||||
accountUuid: 'acct-test',
|
||||
}
|
||||
: undefined,
|
||||
getRateLimitTier: () => null,
|
||||
getSubscriptionType: () => null,
|
||||
}))
|
||||
|
||||
mock.module('./config.js', () => ({
|
||||
getGlobalConfig: () => ({}),
|
||||
getOrCreateUserID: () => 'device-test',
|
||||
}))
|
||||
|
||||
mock.module('./cwd.js', () => ({
|
||||
getCwd: () => 'C:\\repo',
|
||||
}))
|
||||
|
||||
mock.module('./env.js', () => ({
|
||||
env: { platform: 'windows' },
|
||||
getHostPlatformForAnalytics: () => 'windows',
|
||||
}))
|
||||
|
||||
mock.module('./envUtils.js', () => ({
|
||||
isEnvTruthy: (value: string | undefined) =>
|
||||
!!value && value !== '0' && value.toLowerCase() !== 'false',
|
||||
}))
|
||||
|
||||
mock.module('execa', () => ({
|
||||
execa: async () => ({
|
||||
exitCode: options?.gitEmail ? 0 : 1,
|
||||
stdout: options?.gitEmail ?? '',
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
process.env = { ...originalEnv }
|
||||
delete (globalThis as Record<string, unknown>).MACRO
|
||||
})
|
||||
|
||||
describe('user email fallbacks', () => {
|
||||
test('getCoreUserData does not synthesize Anthropic email from COO_CREATOR', async () => {
|
||||
process.env.USER_TYPE = 'ant'
|
||||
process.env.COO_CREATOR = 'alice'
|
||||
;(globalThis as Record<string, unknown>).MACRO = { VERSION: '0.0.0' }
|
||||
|
||||
installCommonMocks()
|
||||
|
||||
const { getCoreUserData } = await importFreshUserModule()
|
||||
const result = getCoreUserData()
|
||||
|
||||
expect(result.email).toBeUndefined()
|
||||
})
|
||||
|
||||
test('initUser falls back to git email when oauth email is missing', async () => {
|
||||
process.env.USER_TYPE = 'ant'
|
||||
process.env.COO_CREATOR = 'alice'
|
||||
;(globalThis as Record<string, unknown>).MACRO = { VERSION: '0.0.0' }
|
||||
|
||||
installCommonMocks({ gitEmail: 'git@example.com' })
|
||||
|
||||
const { initUser, getCoreUserData } = await importFreshUserModule()
|
||||
await initUser()
|
||||
|
||||
const result = getCoreUserData()
|
||||
expect(result.email).toBe('git@example.com')
|
||||
})
|
||||
})
|
||||
@@ -146,15 +146,6 @@ function getEmail(): string | undefined {
|
||||
return oauthAccount.emailAddress
|
||||
}
|
||||
|
||||
// Ant-only fallbacks below (no execSync)
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (process.env.COO_CREATOR) {
|
||||
return `${process.env.COO_CREATOR}@anthropic.com`
|
||||
}
|
||||
|
||||
// If initUser() wasn't called, we return undefined instead of blocking
|
||||
return undefined
|
||||
}
|
||||
@@ -166,15 +157,6 @@ async function getEmailAsync(): Promise<string | undefined> {
|
||||
return oauthAccount.emailAddress
|
||||
}
|
||||
|
||||
// Ant-only fallbacks below
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (process.env.COO_CREATOR) {
|
||||
return `${process.env.COO_CREATOR}@anthropic.com`
|
||||
}
|
||||
|
||||
return getGitEmail()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user