Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ function gh(method, path, jwt, body) {
})
}

function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}

export async function requestAppInstallationToken(config) {
const {
body,
jwt,
method,
path,
requestImpl = gh,
sleepImpl = sleep,
} = config
for (let attempt = 0; ; attempt += 1) {
const response = await requestImpl(method, path, jwt, body)
if (![500, 502, 503, 504].includes(response.status) || attempt === 2) {
return response
}
await sleepImpl(1000 * 2 ** attempt)
}
}

// Parse a PERMISSIONS string (a JSON object) into the access-token request, or
// undefined when blank. Throws on malformed or empty-object input — an empty
// object would mint a blanket-permission token, the opposite of least-privilege.
Expand Down Expand Up @@ -217,11 +239,32 @@ export function formatAppPermissionShortfall(config) {
}

export function formatAppTokenMintFailure(config) {
const { body, installationId, owner, slug, status } = config
const { installationId, owner, slug, status } = config
const where = `Where: POST /app/installations/${installationId}/access_tokens. `
if (status >= 500 && status <= 599) {
return (
`token mint failed: HTTP ${status}. ` +
where +
'Saw: GitHub returned a server error while creating the installation token. ' +
'Fix: retry the workflow later. Do not expand App permissions based on a GitHub server error.'
)
}
const responseMessage =
status === 422
? 'GitHub rejected the requested repositories or permissions.'
: 'GitHub rejected the installation-token request.'
if (status !== 422) {
return (
`token mint failed: HTTP ${status}. ` +
where +
`Saw: ${responseMessage} ` +
`Fix: verify the ${slug} App credentials, its installation on ${owner}, and repository access.`
)
}
return (
`token mint failed: HTTP ${status}. ` +
`Where: POST /app/installations/${installationId}/access_tokens. ` +
`Saw: ${body}. Fix: the requested permissions/repositories must be ` +
where +
`Saw: ${responseMessage}. Fix: the requested permissions/repositories must be ` +
`a subset of what the app's installation on ${owner} grants. ` +
`${formatAppRegistrationGuidance(slug)} ` +
'Open Permissions & events -> Repository permissions. ' +
Expand Down Expand Up @@ -333,16 +376,15 @@ async function main() {
tokenBody.permissions = permissions
}
tokenBody.repositories = repositories
const minted = await gh(
'POST',
`/app/installations/${installationId}/access_tokens`,
const minted = await requestAppInstallationToken({
body: JSON.stringify(tokenBody),
jwt,
JSON.stringify(tokenBody),
)
method: 'POST',
path: `/app/installations/${installationId}/access_tokens`,
})
if (minted.status !== 201) {
die(
formatAppTokenMintFailure({
body: minted.body,
installationId,
owner,
slug: installation.app_slug ?? '<app>',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ function gh(method, path, jwt, body) {
})
}

function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}

export async function requestAppInstallationToken(config) {
const {
body,
jwt,
method,
path,
requestImpl = gh,
sleepImpl = sleep,
} = config
for (let attempt = 0; ; attempt += 1) {
const response = await requestImpl(method, path, jwt, body)
if (![500, 502, 503, 504].includes(response.status) || attempt === 2) {
return response
}
await sleepImpl(1000 * 2 ** attempt)
}
}

// Parse a PERMISSIONS string (a JSON object) into the access-token request, or
// undefined when blank. Throws on malformed or empty-object input — an empty
// object would mint a blanket-permission token, the opposite of least-privilege.
Expand Down Expand Up @@ -217,11 +239,32 @@ export function formatAppPermissionShortfall(config) {
}

export function formatAppTokenMintFailure(config) {
const { body, installationId, owner, slug, status } = config
const { installationId, owner, slug, status } = config
const where = `Where: POST /app/installations/${installationId}/access_tokens. `
if (status >= 500 && status <= 599) {
return (
`token mint failed: HTTP ${status}. ` +
where +
'Saw: GitHub returned a server error while creating the installation token. ' +
'Fix: retry the workflow later. Do not expand App permissions based on a GitHub server error.'
)
}
const responseMessage =
status === 422
? 'GitHub rejected the requested repositories or permissions.'
: 'GitHub rejected the installation-token request.'
if (status !== 422) {
return (
`token mint failed: HTTP ${status}. ` +
where +
`Saw: ${responseMessage} ` +
`Fix: verify the ${slug} App credentials, its installation on ${owner}, and repository access.`
)
}
return (
`token mint failed: HTTP ${status}. ` +
`Where: POST /app/installations/${installationId}/access_tokens. ` +
`Saw: ${body}. Fix: the requested permissions/repositories must be ` +
where +
`Saw: ${responseMessage}. Fix: the requested permissions/repositories must be ` +
`a subset of what the app's installation on ${owner} grants. ` +
`${formatAppRegistrationGuidance(slug)} ` +
'Open Permissions & events -> Repository permissions. ' +
Expand Down Expand Up @@ -333,16 +376,15 @@ async function main() {
tokenBody.permissions = permissions
}
tokenBody.repositories = repositories
const minted = await gh(
'POST',
`/app/installations/${installationId}/access_tokens`,
const minted = await requestAppInstallationToken({
body: JSON.stringify(tokenBody),
jwt,
JSON.stringify(tokenBody),
)
method: 'POST',
path: `/app/installations/${installationId}/access_tokens`,
})
if (minted.status !== 201) {
die(
formatAppTokenMintFailure({
body: minted.body,
installationId,
owner,
slug: installation.app_slug ?? '<app>',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ function gh(method, path, jwt, body) {
})
}

function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}

export async function requestAppInstallationToken(config) {
const {
body,
jwt,
method,
path,
requestImpl = gh,
sleepImpl = sleep,
} = config
for (let attempt = 0; ; attempt += 1) {
const response = await requestImpl(method, path, jwt, body)
if (![500, 502, 503, 504].includes(response.status) || attempt === 2) {
return response
}
await sleepImpl(1000 * 2 ** attempt)
}
}

// Parse a PERMISSIONS string (a JSON object) into the access-token request, or
// undefined when blank. Throws on malformed or empty-object input — an empty
// object would mint a blanket-permission token, the opposite of least-privilege.
Expand Down Expand Up @@ -217,11 +239,32 @@ export function formatAppPermissionShortfall(config) {
}

export function formatAppTokenMintFailure(config) {
const { body, installationId, owner, slug, status } = config
const { installationId, owner, slug, status } = config
const where = `Where: POST /app/installations/${installationId}/access_tokens. `
if (status >= 500 && status <= 599) {
return (
`token mint failed: HTTP ${status}. ` +
where +
'Saw: GitHub returned a server error while creating the installation token. ' +
'Fix: retry the workflow later. Do not expand App permissions based on a GitHub server error.'
)
}
const responseMessage =
status === 422
? 'GitHub rejected the requested repositories or permissions.'
: 'GitHub rejected the installation-token request.'
if (status !== 422) {
return (
`token mint failed: HTTP ${status}. ` +
where +
`Saw: ${responseMessage} ` +
`Fix: verify the ${slug} App credentials, its installation on ${owner}, and repository access.`
)
}
return (
`token mint failed: HTTP ${status}. ` +
`Where: POST /app/installations/${installationId}/access_tokens. ` +
`Saw: ${body}. Fix: the requested permissions/repositories must be ` +
where +
`Saw: ${responseMessage}. Fix: the requested permissions/repositories must be ` +
`a subset of what the app's installation on ${owner} grants. ` +
`${formatAppRegistrationGuidance(slug)} ` +
'Open Permissions & events -> Repository permissions. ' +
Expand Down Expand Up @@ -333,16 +376,15 @@ async function main() {
tokenBody.permissions = permissions
}
tokenBody.repositories = repositories
const minted = await gh(
'POST',
`/app/installations/${installationId}/access_tokens`,
const minted = await requestAppInstallationToken({
body: JSON.stringify(tokenBody),
jwt,
JSON.stringify(tokenBody),
)
method: 'POST',
path: `/app/installations/${installationId}/access_tokens`,
})
if (minted.status !== 201) {
die(
formatAppTokenMintFailure({
body: minted.body,
installationId,
owner,
slug: installation.app_slug ?? '<app>',
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ jobs:
- name: Validate repository publish contract
run: pnpm run --if-present prepublish:check
- name: Set up odai
if: ${{ inputs.resume-publish != true && inputs.release-as == '' && inputs.nightly-version == '' }}
if: ${{ inputs.resume-publish != true }}
uses: ./.github/actions/fleet/setup-odai
- name: 'Mint PR branch token'
if: ${{ steps.content-trust.outputs.trusted == 'true' }}
Expand Down
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.2.0](https://github.com/SocketDev/socket-lib/releases/tag/v7.2.0) - 2026-10-18

### Fixed

- **`release`** — skip model setup for explicit targets
- **`node`** — support Node 18 array primordials
- **`node`** — support npm 10 on Node 18
- **`node`** — support Node 18.20.8 runtime
- **`release`** — resume from reserved bump source
- **`release`** — use hosted npm runner

### Internal

- **`ci`** — track publish coverage workflow
- **`ci`** — repair required workflow gates
- **`ci`** — remove consumer freshness gate
- **`ci`** — move quality audits to follow-ups

## [7.1.1](https://github.com/SocketDev/socket-lib/releases/tag/v7.1.1) - 2026-10-15

### Fixed
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@socketsecurity/lib",
"version": "7.1.1",
"version": "7.2.0",
"description": "Core utilities and infrastructure for Socket.dev security tools",
"keywords": [
"Socket.dev",
Expand Down Expand Up @@ -4154,8 +4154,6 @@
"setup:token:rotate": "node scripts/fleet/setup/token.mts --rotate",
"setup:xcode": "node scripts/fleet/setup/xcode-license.mts",
"setup:xcode-license": "node scripts/fleet/setup/xcode-license.mts",
"slack:auth": "node scripts/fleet/setup/slack-token.mts",
"slack:token": "node scripts/fleet/setup/slack-token.mts",
"soak:bypass": "node scripts/fleet/soak-bypass.mts",
"soak:check": "node scripts/fleet/check/soak-time-is-consistent.mts",
"socket-wheelhouse:emit-schema": "node scripts/fleet/socket-wheelhouse-emit-schema.mts",
Expand Down
20 changes: 0 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading