npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@creativeorange/azure-text-to-speech

v3.0.2

Published

Browser package for Azure Cognitive Services Speech with **temporary authorization tokens**. Permanent Azure subscription keys never enter frontend JavaScript.

Downloads

1,069

Readme

Azure Text to Speech / Speech to Text

Browser package for Azure Cognitive Services Speech with temporary authorization tokens. Permanent Azure subscription keys never enter frontend JavaScript.

Package: @creativeorange/azure-text-to-speech
Current major version: 3.0.0

Why version 3 exists

In version 2, constructors accepted a permanent Azure subscription key and used fromSubscription. That forced the real API key into browser bundles, DevTools, and memory.

Version 3 removes that pattern completely. The permanent key stays on your server. The browser only receives short-lived Azure authorization tokens via your own backend.

Architecture

Browser
  → your backend token endpoint
  → Azure issueToken endpoint
  ← temporary Azure token + region
  → Azure Speech SDK (fromAuthorizationToken)

Supported authentication options:

  1. tokenEndpoint — simple GET endpoint (Craft, classic sites)
  2. getAuthorizationToken — custom async provider

There is no option to pass a permanent subscription key.

The Azure Speech SDK is bundled into the published dist files, so consumers do not need to install microsoft-cognitiveservices-speech-sdk separately. That dependency remains a devDependency of this package.

Install

npm install @creativeorange/azure-text-to-speech

TypeScript declarations are published at dist/main.d.ts.

import {
    TextToSpeech,
    SpeechToText,
    type TextToSpeechOptions,
    type SpeechToTextOptions,
    type SpeechAuthorization,
} from '@creativeorange/azure-text-to-speech';

Always import from the package root. Do not import from /dist/... paths.

Public API

Text to Speech with tokenEndpoint

const textToSpeech = new TextToSpeech({
    tokenEndpoint: '/actions/azure-speech/token',
    voice: 'nl-NL-FennaNeural',
});

await textToSpeech.start();

The Speech region comes from the token endpoint response, not from frontend configuration. That prevents mismatches between frontend, backend and Azure resource region.

Text to Speech with a custom provider

const textToSpeech = new TextToSpeech({
    voice: 'nl-NL-FennaNeural',
    getAuthorizationToken: async () => {
        const response = await fetch('/api/azure-speech/token', {
            credentials: 'same-origin',
            cache: 'no-store',
            headers: {
                Accept: 'application/json',
            },
        });

        if (!response.ok) {
            throw new Error('Could not obtain Azure Speech token');
        }

        return response.json();
    },
});

await textToSpeech.start();

Speech to Text

const speechToText = new SpeechToText({
    tokenEndpoint: '/actions/azure-speech/token',
    sourceLanguage: 'nl-NL',
    targetLanguage: 'nl',
});

await speechToText.start();

Continuous recognition can outlive a single Azure token. After recognition starts successfully, Speech-to-Text refreshes the active recognizer token about every eight minutes (tokenLifetimeMs) by setting recognizer.authorizationToken.

Token endpoint response

{
    "token": "temporary-azure-token",
    "region": "westeurope"
}

Options

type SpeechAuthorization = {
    token: string;
    region: string;
};

type SpeechAuthenticationOptions = {
    tokenEndpoint?: string;
    getAuthorizationToken?: () => Promise<SpeechAuthorization>;
    tokenRequestOptions?: {
        headers?: Record<string, string>;
        credentials?: RequestCredentials;
        cache?: RequestCache; // default: 'no-store'
    };
    tokenLifetimeMs?: number; // default: 8 minutes
};

type TextToSpeechOptions = SpeechAuthenticationOptions & {
    voice: string;
    rate?: number;
    pitch?: number;
    url?: string; // lexicon URL
};

type SpeechToTextOptions = SpeechAuthenticationOptions & {
    sourceLanguage: string;
    targetLanguage?: string;
};

Provide exactly one of tokenEndpoint or getAuthorizationToken.

Token caching and transport

  • Tokens are cached in memory and reused while valid.
  • Tokens refresh after about 8 minutes by default.
  • Parallel callers share one in-flight request.
  • tokenEndpoint requests use credentials: 'same-origin' and cache: 'no-store' by default.
  • Tokens are never logged and never included in error events.

Browser events

TTS:

  • COAzureTTSStartedPlaying
  • COAzureTTSFinishedPlaying
  • COAzureTTSStoppedPlaying
  • COAzureTTSPausedPlaying
  • COAzureTTSResumedPlaying
  • COAzureTTSError{ detail: { error: { message, code? } } }

STT:

  • COAzureSTTStartedRecording
  • COAzureSTTStoppedRecording
  • COAzureSTTError{ detail: { error: { message, code? } } }

Migrating from v2 to v3

Old unsafe usage

const textToSpeech = new TextToSpeech(
    window.azureSpeechKey,
    'westeurope',
    'nl-NL-FennaNeural',
    0,
    0
);

Remove Twig/Vite/window exports of the subscription key.

New safe usage

const textToSpeech = new TextToSpeech({
    tokenEndpoint: '/actions/azure-speech/token',
    voice: 'nl-NL-FennaNeural',
    rate: 0,
    pitch: 0,
});

Migration checklist

  1. Update the package to 3.x.
  2. Store AZURE_SPEECH_KEY and AZURE_SPEECH_REGION server-side only.
  3. Add a backend token endpoint.
  4. Replace the old constructor with the options object.
  5. Remove every frontend variable that contained the subscription key.
  6. Rebuild the frontend.
  7. Confirm in DevTools that the permanent key no longer appears.
  8. Test TTS, STT, highlighting, pause/resume, chained playback and prefetching.
  9. Rotate the previously exposed Azure subscription key.

Craft CMS integration

.env

AZURE_SPEECH_KEY="replace-with-secret-key"
AZURE_SPEECH_REGION="westeurope"

Never export these values to Twig globals, Vite define, or frontend env files.

Module registration (config/app.php)

<?php

return [
    'modules' => [
        'azure-speech' => [
            'class' => \modules\azurespeech\Module::class,
        ],
    ],
    'bootstrap' => [
        'azure-speech',
    ],
];

Craft action routing uses:

  • module ID: azure-speech
  • controller ID: token (TokenController)
  • action: actionIndex

Resulting URL:

/actions/azure-speech/token

Ensure the module registers its controller namespace, for example in modules/azurespeech/Module.php:

public function init(): void
{
    parent::init();
    // Controllers live in modules\azurespeech\controllers
}

Token controller for Craft 4 / Craft 5

<?php

namespace modules\azurespeech\controllers;

use Craft;
use craft\web\Controller;
use yii\web\Response;

class TokenController extends Controller
{
    // Craft 4 and Craft 5
    protected array|bool|int $allowAnonymous = true;

    public function actionIndex(): Response
    {
        $key = Craft::parseEnv('$AZURE_SPEECH_KEY');
        $region = Craft::parseEnv('$AZURE_SPEECH_REGION');

        if (!$key || !$region) {
            return $this->asFailure(
                'Azure Speech configuration is missing.'
            );
        }

        $client = Craft::createGuzzleClient();

        try {
            $azureResponse = $client->post(
                "https://{$region}.api.cognitive.microsoft.com/sts/v1.0/issueToken",
                [
                    'headers' => [
                        'Ocp-Apim-Subscription-Key' => $key,
                    ],
                    'timeout' => 10,
                ]
            );
        } catch (\Throwable $exception) {
            Craft::error(
                'Could not retrieve an Azure Speech token.',
                __METHOD__
            );

            return $this->asFailure(
                'Speech authorization is temporarily unavailable.'
            );
        }

        $craftResponse = $this->asJson([
            'token' => (string) $azureResponse->getBody(),
            'region' => $region,
        ]);

        $craftResponse->getHeaders()->set(
            'Cache-Control',
            'private, no-store, max-age=0'
        );
        $craftResponse->getHeaders()->set(
            'Pragma',
            'no-cache'
        );

        return $craftResponse;
    }
}

Token controller property for Craft 3 / older PHP

// Craft 3 or older PHP versions without union property types
protected $allowAnonymous = true;

Use the property form that matches your Craft and PHP version. Do not log tokens or exception details to the browser.

Frontend initialization

import {TextToSpeech, SpeechToText} from '@creativeorange/azure-text-to-speech';

const textToSpeech = new TextToSpeech({
    tokenEndpoint: '/actions/azure-speech/token',
    voice: 'nl-NL-FennaNeural',
});

textToSpeech.start().catch((error) => {
    console.error('Could not initialize text to speech.', error);
});

const speechToText = new SpeechToText({
    tokenEndpoint: '/actions/azure-speech/token',
    sourceLanguage: 'nl-NL',
    targetLanguage: 'nl',
});

speechToText.start().catch((error) => {
    console.error('Could not initialize speech to text.', error);
});

Protecting the Craft token endpoint

A temporary token may appear in the browser. That does not mean the endpoint can be public and unlimited.

Recommended controls:

  • Craft rate limiting and/or webserver/proxy rate limiting
  • Cloudflare rate limiting when available
  • Authentication when TTS/STT is not meant for anonymous visitors
  • HTTPS only
  • Short server-side timeouts
  • Azure budget and usage alerts
  • Azure resource networking restrictions when infrastructure allows it
  • Never put the permanent key in responses or logs
  • Return Cache-Control: private, no-store, max-age=0

CORS alone is not security. Endpoints can be called outside a browser.

Laravel token endpoint

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;

Route::get('/api/azure-speech/token', function () {
    $region = config('services.azure_speech.region');

    $azureResponse = Http::timeout(10)
        ->withHeaders([
            'Ocp-Apim-Subscription-Key' =>
                config('services.azure_speech.key'),
        ])
        ->post(
            "https://{$region}.api.cognitive.microsoft.com/sts/v1.0/issueToken"
        );

    abort_unless($azureResponse->successful(), 502);

    return response()
        ->json([
            'token' => $azureResponse->body(),
            'region' => $region,
        ])
        ->header(
            'Cache-Control',
            'private, no-store, max-age=0'
        )
        ->header('Pragma', 'no-cache');
})->middleware('throttle:20,1');

Put this route behind authentication whenever the feature is not public.

Existing browser features

Version 3 keeps the existing client behaviour from the development branch:

  • play / pause / resume / stop
  • word-boundary highlighting
  • chained playback (co-tts.next)
  • audio prefetching
  • lexicons via url
  • voice / rate / pitch setters
  • existing custom browser events

Development

npm ci
npm run lint
npm test
npm run build
npm run typecheck

Security sanity checks:

grep -R "fromSubscription" src
grep -R "subscriptionKey" src
grep -R "Ocp-Apim-Subscription-Key" src
grep -R "@creativeorange/azure-text-to-speech/dist" .

These should return no matches in package source or active examples.

License

MIT