Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/cowswap-frontend/src/locales/en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -3182,8 +3182,8 @@ msgid "Rewards will be sent on Ethereum to"
msgstr "Rewards will be sent on Ethereum to"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx
msgid "Bridging without swapping is not yet supported. Let us know if you want this feature!"
msgstr "Bridging without swapping is not yet supported. Let us know if you want this feature!"
#~ msgid "Bridging without swapping is not yet supported. Let us know if you want this feature!"
#~ msgstr "Bridging without swapping is not yet supported. Let us know if you want this feature!"

#: apps/cowswap-frontend/src/modules/affiliate/containers/AffiliateTraderCodeInfo.tsx
msgid "Rewards end"
Expand Down Expand Up @@ -7128,8 +7128,8 @@ msgid "Advanced"
msgstr "Advanced"

#: apps/cowswap-frontend/src/modules/tradeFormValidation/pure/TradeFormButtons/tradeButtonsMap.tsx
msgid "Not yet supported"
msgstr "Not yet supported"
#~ msgid "Not yet supported"
#~ msgstr "Not yet supported"

#: apps/cowswap-frontend/src/modules/affiliate/pure/AffiliateTraderModal/AffiliateTradeCodeForm.tsx
#: apps/cowswap-frontend/src/modules/hooksStore/pure/HookListItem/index.tsx
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ exports[`useTradeQuotePolling() When wallet is connected Then should put account
"validFor": 1800,
},
{
"allowIntermediateEqSellToken": true,
"appData": undefined,
"getCorrelatedTokens": [Function],
"getSlippageSuggestion": undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ describe('fetchAndProcessQuote', () => {
quoteRequest: {
priceQuality: PriceQuality.FAST,
},
allowIntermediateEqSellToken: true,
appData: mockAppData,
quoteSigner: undefined,
getSlippageSuggestion: undefined,
Expand Down Expand Up @@ -214,6 +215,7 @@ describe('fetchAndProcessQuote', () => {
quoteRequest: {
priceQuality: PriceQuality.FAST,
},
allowIntermediateEqSellToken: true,
appData: mockAppData,
quoteSigner: {},
}),
Expand Down Expand Up @@ -251,6 +253,7 @@ describe('fetchAndProcessQuote', () => {
quoteRequest: {
priceQuality: PriceQuality.OPTIMAL,
},
allowIntermediateEqSellToken: true,
appData: mockAppData,
quoteSigner: undefined,
getSlippageSuggestion: undefined,
Expand Down Expand Up @@ -285,6 +288,7 @@ describe('fetchAndProcessQuote', () => {
quoteRequest: {
priceQuality: PriceQuality.FAST,
},
allowIntermediateEqSellToken: true,
appData: mockAppData,
quoteSigner: undefined,
getSlippageSuggestion: undefined,
Expand Down Expand Up @@ -684,6 +688,7 @@ describe('fetchAndProcessQuote', () => {
quoteRequest: {
priceQuality: PriceQuality.FAST,
},
allowIntermediateEqSellToken: true,
appData: undefined,
quoteSigner: undefined,
getSlippageSuggestion: undefined,
Expand Down
63 changes: 44 additions & 19 deletions tools/scripts/install.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { execSync } = require('child_process')
const fs = require('fs')
const os = require('os')
const path = require('path')

const ROOT_DIR = path.resolve(__dirname, '../..')
Expand Down Expand Up @@ -189,10 +190,10 @@ function stripLockfileEntries(keys) {
* Runs pnpm install in the repository root.
*
* Uses a frozen lockfile by default. When `authToken` is provided (SDK preview install)
* it injects the GitHub Packages registry/auth (and an optional rewrite pnpmfile) into
* the child pnpm process via `npm_config_*` env vars, then runs with
* `--no-frozen-lockfile`. The auth token never touches the filesystem, so a crash,
* SIGKILL, or Ctrl-C cannot leave a secret in the tracked `.npmrc`.
* it injects the GitHub Packages registry/auth through a temporary npmrc userconfig
* (and an optional rewrite pnpmfile) into the child pnpm process, then runs with
* `--no-frozen-lockfile`. The auth token is written only to an OS temp directory
* npmrc with restrictive permissions and is removed after the install attempt.
*/
function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) {
if (!authToken) {
Expand All @@ -211,22 +212,21 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) {

console.log('[install.js] Installing with SDK PR version (pnpm install --no-frozen-lockfile)...')

// pnpm follows the npm convention of reading config from `npm_config_<key>` env vars.
// Passing the registry/auth this way keeps the token off disk entirely.
const childEnv = {
...process.env,
'npm_config_@cowprotocol:registry': 'https://npm.pkg.github.com',
'npm_config_//npm.pkg.github.com/:_authToken': authToken,
}
Comment on lines -214 to -220

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had no effect on cf-pages

const childEnv = { ...process.env }

if (hasRewrites) {
// `pnpmfile` is a pnpm-specific config key (no secret); pnpm resolves it relative to cwd.
childEnv.npm_config_pnpmfile = TEMP_PNPMFILE_PATH
}

let originalLockfile = null
let tempNpmrcDir = null

try {
const tempNpmrc = createTempNpmrc(authToken)
tempNpmrcDir = tempNpmrc.dir
childEnv.npm_config_userconfig = tempNpmrc.path

if (hasRewrites) createTempPnpmfile(rewriteMap)
if (hasForceResolve) originalLockfile = stripLockfileEntries(forceResolveKeys)

Expand All @@ -238,7 +238,7 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) {
} finally {
if (hasRewrites) {
try {
removeTempPnpmfile()
fs.rmSync(TEMP_PNPMFILE_PATH, { force: true })
} catch (err) {
console.warn('[install.js] Failed to remove temp pnpmfile:', err)
}
Expand All @@ -253,9 +253,41 @@ function runPnpmInstall(authToken, rewriteMap = {}, forceResolveKeys = []) {
console.warn('[install.js] Failed to restore pnpm-lock.yaml:', err)
}
}
if (tempNpmrcDir !== null) {
try {
fs.rmSync(tempNpmrcDir, { force: true, recursive: true })
} catch (err) {
console.warn('[install.js] Failed to remove temp npmrc:', err)
}
}
}
}

/**
* Creates a temporary npmrc containing the GitHub Packages registry/auth settings.
*
* pnpm 10 reliably consumes these scoped auth settings from an npmrc passed through
* `npm_config_userconfig`; passing the same keys directly as environment variables is
* not portable across build providers.
*/
function createTempNpmrc(authToken) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cowswap-pnpm-'))
const npmrcPath = path.join(dir, '.npmrc')

try {
fs.writeFileSync(
npmrcPath,
`@cowprotocol:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${authToken}\n`,
{ mode: 0o600 },
)
} catch (err) {
fs.rmSync(dir, { force: true, recursive: true })
throw err
}

return { dir, path: npmrcPath }
}

/**
* Creates a temporary pnpm hook file that rewrites workspace manifests in memory.
*
Expand Down Expand Up @@ -285,10 +317,3 @@ function createTempPnpmfile(rewriteMap) {

fs.writeFileSync(TEMP_PNPMFILE_PATH, pnpmfile)
}

/**
* Deletes the temporary pnpm hook file if it exists.
*/
function removeTempPnpmfile() {
fs.rmSync(TEMP_PNPMFILE_PATH, { force: true })
}
171 changes: 171 additions & 0 deletions tools/scripts/install.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { describe, it } from 'node:test'
import vm from 'node:vm'

describe('install.js', () => {
it('passes GitHub Packages auth to pnpm through a temporary npmrc', () => {
const context = createInstallScriptContext()

context.run()

assert.equal(context.execCalls.length, 1)
const [{ command, options }] = context.execCalls

assert.equal(command, 'pnpm install --no-frozen-lockfile')
assert.equal(options.cwd, '/repo')
assert.equal(options.env.npm_config_userconfig, '/tmp/cowswap-pnpm-test/.npmrc')
assert.equal(options.env['npm_config_//npm.pkg.github.com/:_authToken'], undefined)
assert.equal(options.env['npm_config_@cowprotocol:registry'], undefined)

assertTempNpmrcWrittenAndRemoved(context)
})

it('removes the temporary npmrc when pnpm install fails', () => {
const failure = new Error('fail')
const exitError = new Error('process.exit')
let exitCode
const context = createInstallScriptContext({
execSync() {
throw failure
},
processExit(code) {
exitCode = code
throw exitError
},
})
let caughtError

try {
context.run()
} catch (err) {
caughtError = err
}

assert.equal(caughtError, exitError)
assert.equal(exitCode, 1)
assertTempNpmrcWrittenAndRemoved(context)
})
})

function createInstallScriptContext({ execSync = () => {}, processExit = defaultProcessExit } = {}) {
const script = readFileSync(new URL('./install.js', import.meta.url), 'utf8')
const execCalls = []
const writes = []
const removals = []

const fakeFs = {
existsSync() {
return false
},
mkdtempSync(prefix) {
assert.equal(prefix, '/tmp/cowswap-pnpm-')
return '/tmp/cowswap-pnpm-test'
},
readFileSync() {
throw new Error('unexpected fs.readFileSync call')
},
readdirSync() {
throw new Error('unexpected fs.readdirSync call')
},
rmSync(path, options) {
removals.push({ path, options })
},
writeFileSync(path, content, options) {
writes.push({ path, content, options })
},
}

const sandbox = {
__dirname: '/repo/tools/scripts',
console: {
error() {},
log() {},
warn() {},
},
process: {
env: { PACKAGE_READ_AUTH_TOKEN: 'secret-token' },
exit(code) {
processExit(code)
},
},
require(id) {
if (id === 'child_process') {
return {
execSync(command, options) {
execCalls.push({ command, options })
execSync(command, options)
},
}
}

if (id === 'fs') return fakeFs
if (id === 'os') {
return {
tmpdir() {
return '/tmp'
},
}
}
if (id === 'path') return pathModule
if (id === '../../apps/cowswap-frontend/package.json') {
return {
dependencies: {
'@cowprotocol/sdk-bridging': '4.1.2-pr-897-c3d02aa9.0',
},
}
}

throw new Error(`unexpected require(${id})`)
},
}

return {
execCalls,
removals,
run() {
vm.runInNewContext(
`(function(require, process, console, __dirname) {\n${script}\n})(require, process, console, __dirname)`,
sandbox,
)
},
writes,
}
}

function defaultProcessExit(code) {
throw new Error(`unexpected process.exit(${code})`)
}

function assertTempNpmrcWrittenAndRemoved({ removals, writes }) {
assert.equal(writes.length, 1)
assert.equal(writes[0].path, '/tmp/cowswap-pnpm-test/.npmrc')
assert.equal(
writes[0].content,
'@cowprotocol:registry=https://npm.pkg.github.com\n' + '//npm.pkg.github.com/:_authToken=secret-token\n',
)
assert.equal(writes[0].options.mode, 0o600)

assert.equal(removals.length, 1)
assert.equal(removals[0].path, '/tmp/cowswap-pnpm-test')
assert.equal(removals[0].options.force, true)
assert.equal(removals[0].options.recursive, true)
}

const pathModule = {
join(...parts) {
return parts.join('/').replace(/\/+/g, '/')
},
resolve(...parts) {
const joined = parts.join('/')
const segments = []

for (const segment of joined.split('/')) {
if (!segment || segment === '.') continue
if (segment === '..') segments.pop()
else segments.push(segment)
}

return '/' + segments.join('/')
},
}
Loading