* ci: authenticate to AWS via GitHub OIDC instead of stored access keys
CI held a long-lived AWS access key as repository secrets. This replaces it with
a short-lived credential obtained through the GitHub OIDC provider, so no AWS key
is stored in the repo at all.
The tests could not simply inherit the OIDC credentials. They passed
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY explicitly, which routes
lib/get-aws-sts-token.js down its accessKeyId branch and calls GetSessionToken --
and AWS rejects GetSessionToken when it is called with session credentials.
test/aws-credentials.js centralises the decision. When AWS_SESSION_TOKEN is present
the credentials are temporary and only the region is passed, so the SDK's default
credential provider chain is used. Static keys still work unchanged, which keeps
local run-tests.sh working and also lets it run off an 'aws sso login' session with
no credentials in the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: supply a cache key when AWS credentials come from the default chain
getAwsAuthToken derives its cache key as roleArn || accessKeyId || speech_credential_sid.
With temporary credentials the test passes none of those, so makeAwsKey received
undefined and hash.update() threw ERR_INVALID_ARG_TYPE.
Production never hits this: the instance-profile path always carries a
speech_credential_sid from the database, which is exactly what the comment above
that line describes. The test now supplies one the same way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci: reference the CI role via secret rather than inlining the account id
speech-utils is public, so the role ARN (and with it the AWS account id) should not
be committed. It now comes from the AWS_ROLE_ARN secret, which also feeds the
'AWS speech synth tests by RoleArn' test -- previously always skipped, so the
AssumeRole credential path had no coverage here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: give the AWS RoleArn synth test its own cache key
The RoleArn test synthesized the same vendor/voice/language/text as the plain AWS
synth test that runs before it, so it hit that test's cache entry, servedFromCache
came back true and the !servedFromCache assertion failed.
Latent since the test was written -- it never ran, because AWS_ROLE_ARN was never
supplied. Wiring the secret in activated it and exposed the collision. Distinct text
makes the test independent of execution order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Adds synthFishaudio with both arms: the say: streaming url consumed by the
mediajam dialect, and a POST /v1/tts cache render. The render asks for raw pcm
at 8k and returns extension r8 because fish's wav output carries a placeholder
RIFF size, the same problem gradium has.
Fish is a voice-cloning vendor, so the voice is a reference_id; the sentinel
'default' means send none and use fish's own default voice.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The vendor replaced its voice catalog and now rejects unknown ids, so
the env-gated test failed whenever NINENINESIX_API_KEY was set.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The test I added in #155 ran unconditionally, which broke `npm test` without
credentials — the path husky's pre-commit hook takes, so `npm version patch`
could not commit.
Two causes, both addressed:
- no credential gate, unlike every other vendor test in this file. Now skips
without INWORLD_API_KEY, and closes its redis client on that path so the
run can still exit.
- it assumed streaming was enabled. The Google non-streaming test sets
JAMBONES_DISABLE_TTS_STREAMING and, on its no-credentials skip path,
deletes the env var WITHOUT clearing the require cache (unlike its finally
block, which clears both) — so lib/config still held 'true' further down
the file and synthInworld took the non-streaming branch, attempting a real
vendor call. The test now re-requires with streaming enabled so it does not
depend on what ran before it.
Verified both ways: skips and exits 0 with no key; 11/11 with a key even
under the leaked state. Re-introducing the #155 bug still fails 3 assertions,
so the regression value is intact.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The streaming say: path guarded on opts.audioConfig?.pitch and
opts.audioConfig?.speakingRate but interpolated opts.pitch and
opts.speakingRate, which are undefined — so anyone setting them under
audioConfig (what the docs and the portal defaults tell you to do) got
'pitch=undefined,speakingRate=undefined' on the wire and their setting
silently dropped.
Adds a test for the say: params that needs no credentials, since the
streaming branch builds the path without calling the vendor.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Streaming arm returns a say: url for the mediajam dialect; the cache-render
arm posts to /api/post/speech/tts with only_audio and pcm_8000, which is bare
r8 samples and avoids gradium's streaming wav header (0xffffffff RIFF size).
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Streaming goes through mediajam's say: url; the cache render posts to
/tts/bytes for wav, since the service rejects mp3.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore: deprecate and remove verbio, nuance speech vendor support
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: also deprecate and remove PlayHT speech vendor
PlayHT was acquired and no longer provides the service.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>