Problem
Clients create or update an Actions secret in two steps. They fetch the repository or organization public key, encrypt the value with a libsodium sealed box, and PUT the secret with encrypted_value and key_id. The emulator accepts the PUT request, but it lacks the public key endpoints. The first step fails with 404, so clients never write the secret. Checked on main at b75beb7 with npx emulate@0.13.0.
Reproduction
Seed octocat/hello-world, an organization my-org with octocat as an admin member, and a repro_token for octocat. Then start the GitHub service:
npx emulate@0.13.0 start --service github --port 4001 --seed emulate.config.yamlcurl -s -i http://localhost:4001/repos/octocat/hello-world/actions/secrets/public-key -H 'Authorization: Bearer repro_token'
curl -s -i http://localhost:4001/orgs/my-org/actions/secrets/public-key -H 'Authorization: Bearer repro_token'Both return:
HTTP/1.1 404 Not Found
{"message":"Not Found","documentation_url":"https://emulate.dev/github"}Writing the secret already works when a client skips the key lookup:
curl -s -i -X PUT http://localhost:4001/repos/octocat/hello-world/actions/secrets/MY_SECRET \
-H 'Authorization: Bearer repro_token' -H 'Content-Type: application/json' \
-d '{"encrypted_value":"c2VhbGVk","key_id":"012345678912345678"}'
# HTTP/1.1 201 Created
curl -s http://localhost:4001/repos/octocat/hello-world/actions/secrets/MY_SECRET -H 'Authorization: Bearer repro_token'
# {"name":"MY_SECRET","visibility":"all","created_at":"2026-11-06T14:31:46.021Z","updated_at":"2026-11-06T14:31:46.021Z"}GitHub behavior
GET /repos/{owner}/{repo}/actions/secrets/public-keyandGET /orgs/{org}/actions/secrets/public-keyreturn200with{ "key_id": "<id>", "key": "<base64 public key>" }. The key is a base64-encoded 32-byte Curve25519 public key, andkey_ididentifies it.- Clients encrypt the secret value with a libsodium sealed box (
crypto_box_seal) using that key, then sendencrypted_value(base64) and the matchingkey_idtoPUT /repos/{owner}/{repo}/actions/secrets/{secret_name}orPUT /orgs/{org}/actions/secrets/{secret_name}. - The
PUTrequest returns201on creation and204on update. For organization secrets,visibilityis required, andselected_repository_idsapplies whenvisibilityisselected. - Access: Anyone with read access to the repository can read its public key. Reading the organization public key and writing secrets require collaborator access, with the
repooradmin:orgscope for personal access tokens (classic).
Docs: https://docs.github.com/en/rest/actions/secrets#get-a-repository-public-key and https://docs.github.com/en/rest/actions/secrets#create-or-update-a-repository-secret
Proposed scope
GET /repos/{owner}/{repo}/actions/secrets/public-keyandGET /orgs/{org}/actions/secrets/public-key, each returning a stablekey_idand a valid base64-encoded Curve25519 public key per repository or organization so libsodium clients can encrypt against it.- Optional: Reject
PUTrequests whenkey_iddoes not match the current key. GitHub docs do not specify the HTTP status code returned for this case, so the emulator can continue accepting requests until confirmed. - Optional: Retain the matching private key so the emulator can decrypt
encrypted_valueand expose the plaintext through an inspection endpoint, similar toGET /_emulate/installation-tokens. This allows tests to assert the value a client wrote. - Out of scope for the initial release: environment, Dependabot, and Codespaces public keys.
Proposed implementation
The secret routes already exist and accept writes:
The emulator can generate a key pair for each repository and organization using Node's built-in crypto.generateKeyPairSync("x25519"), which produces Curve25519 keys compatible with libsodium sealed boxes. Store the key pair alongside the secrets collection. The public key is the raw 32 bytes of the key, base64-encoded. Generated keys can follow the pattern used for generated GitHub App keys: keep them stable across reset() and list them in generatedSecrets.
Activity
0No activity yet.