diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9822d9..da83caf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,5 @@ jobs: run: "pnpm lint:check && pnpm format:check" - name: Test run: pnpm -r test + env: + API_KEY: ${{ secrets.API_KEY }} diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 6e5b00d..a6fe0c2 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -12,6 +12,7 @@ "dependencies": { "@speechmatics/flow-client-react": "workspace:*", "@speechmatics/browser-audio-input-react": "workspace:*", + "@speechmatics/auth": "workspace:*", "@picocss/pico": "^2.0.6", "next": "15.0.1", "react": "19.0.0-rc-69d4b800-20241021", diff --git a/examples/nextjs/src/app/actions.ts b/examples/nextjs/src/app/actions.ts new file mode 100644 index 0000000..53fb346 --- /dev/null +++ b/examples/nextjs/src/app/actions.ts @@ -0,0 +1,12 @@ +'use server'; + +import { createSpeechmaticsJWT } from '@speechmatics/auth'; + +export async function getJWT() { + const apiKey = process.env.API_KEY; + if (!apiKey) { + throw new Error('Please set the API_KEY environment variable'); + } + + return createSpeechmaticsJWT({ type: 'flow', apiKey, ttl: 60 }); +} diff --git a/examples/nextjs/src/app/flow/Component.tsx b/examples/nextjs/src/app/flow/Component.tsx index 0a62db9..d485470 100644 --- a/examples/nextjs/src/app/flow/Component.tsx +++ b/examples/nextjs/src/app/flow/Component.tsx @@ -12,12 +12,11 @@ import { Status } from './Status'; import { ErrorFallback } from '../../lib/components/ErrorFallback'; import { OutputView } from './OutputView'; import { useFlow, useFlowEventListener } from '@speechmatics/flow-client-react'; +import { getJWT } from '../actions'; export default function Component({ - jwt, personas, }: { - jwt: string; personas: Record; }) { const { startConversation, sendAudio, endConversation } = useFlow(); @@ -47,8 +46,12 @@ export default function Component({ }: { personaId: string; deviceId?: string }) => { try { setLoading(true); + + const jwt = await getJWT(); + const audioContext = new AudioContext({ sampleRate: SAMPLE_RATE }); setAudioContext(audioContext); + await startConversation(jwt, { config: { template_id: personaId, @@ -60,13 +63,14 @@ export default function Component({ sample_rate: SAMPLE_RATE, }, }); + const mediaStream = await startRecording(audioContext, deviceId); setMediaStream(mediaStream); } finally { setLoading(false); } }, - [startConversation, jwt, startRecording], + [startConversation, startRecording], ); const stopSession = useCallback(async () => { diff --git a/examples/nextjs/src/app/flow/page.tsx b/examples/nextjs/src/app/flow/page.tsx index fc16870..8150436 100644 --- a/examples/nextjs/src/app/flow/page.tsx +++ b/examples/nextjs/src/app/flow/page.tsx @@ -1,16 +1,12 @@ -import { fetchCredentials } from '@/lib/fetch-credentials'; -import Component from './Component'; import { fetchPersonas, FlowProvider } from '@speechmatics/flow-client-react'; +import Component from './Component'; export default async function Home() { - // Credentials here are being fetched when rendering the server component. - // You could instead define an API action to request it on the fly. - const creds = await fetchCredentials(); const personas = await fetchPersonas(); return ( - + ); } diff --git a/examples/nextjs/src/lib/fetch-credentials.ts b/examples/nextjs/src/lib/fetch-credentials.ts deleted file mode 100644 index 490f8e3..0000000 --- a/examples/nextjs/src/lib/fetch-credentials.ts +++ /dev/null @@ -1,19 +0,0 @@ -export async function fetchCredentials() { - const resp = await fetch( - 'https://mp.speechmatics.com/v1/api_keys?type=flow', - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${process.env.API_KEY}`, - }, - body: JSON.stringify({ - ttl: 3600, - }), - }, - ); - if (!resp.ok) { - throw new Error('Bad response from API', { cause: resp }); - } - return await resp.json(); -} diff --git a/examples/nodejs/batch-example.ts b/examples/nodejs/batch-example.ts index d15dea4..db2219c 100644 --- a/examples/nodejs/batch-example.ts +++ b/examples/nodejs/batch-example.ts @@ -1,3 +1,13 @@ +/** + * This file showcases the batch-client package being used in NodeJS. + * + * It will connect to the batch API and transcribe a file. + * To run this example, you will need to have a Speechmatics API key, + * which can be generated from the Speechmatics Portal: https://portal.speechmatics.com/api-keys + * + * NOTE: This script is run as an ES Module via tsx, letting us use top-level await. + * The library also works with CommonJS, but the code would need to be wrapped in an async function. + */ import { BatchClient } from '@speechmatics/batch-client'; import { openAsBlob } from 'node:fs'; import dotenv from 'dotenv'; @@ -13,26 +23,31 @@ const client = new BatchClient({ apiKey, appId: 'nodeJS-example' }); console.log('Sending file for transcription...'); -(async () => { - const blob = await openAsBlob('./example.wav'); - const file = new File([blob], 'example.wav'); - - const response = await client.transcribe( - file, - { - transcription_config: { - language: 'en', - }, +const blob = await openAsBlob('./example.wav'); +const file = new File([blob], 'example.wav'); + +const response = await client.transcribe( + // You can pass a File object... + file, + // ...or this: + // { data: blob, fileName: 'example.wav' }, + // ...or this: + // { + // url: 'https://github.com/speechmatics/speechmatics-js-sdk/raw/7e0083b830421541091730455f875be2a1984dc6/examples/nodejs/example.wav', + // }, + { + transcription_config: { + language: 'en', }, - 'json-v2', - ); - - console.log('Transcription finished!'); - - console.log( - // Transcripts can be strings when the 'txt' format is chosen - typeof response === 'string' - ? response - : response.results.map((r) => r.alternatives?.[0].content).join(' '), - ); -})(); + }, + 'json-v2', +); + +console.log('Transcription finished!'); + +console.log( + // Transcripts can be strings when the 'txt' format is chosen + typeof response === 'string' + ? response + : response.results.map((r) => r.alternatives?.[0].content).join(' '), +); diff --git a/examples/nodejs/package.json b/examples/nodejs/package.json index 1e89eb6..5ff7b31 100644 --- a/examples/nodejs/package.json +++ b/examples/nodejs/package.json @@ -3,10 +3,10 @@ "version": "1.0.0", "private": "true", "description": "NodeJS examples showcasing the Speechmatics JS SDK", - "main": "index.js", + "type": "module", "scripts": { - "run:batch": "tsx batch-example.ts", - "run:real-time-file": "tsx real-time-file-example.ts" + "run:batch": "node --import tsx/esm batch-example.ts", + "run:real-time-file": "node --import tsx/esm real-time-file-example.ts" }, "keywords": [], "author": "", @@ -14,9 +14,7 @@ "dependencies": { "@speechmatics/batch-client": "workspace:*", "@speechmatics/real-time-client": "workspace:*", + "@speechmatics/auth": "workspace:*", "dotenv": "^16.4.5" - }, - "devDependencies": { - "tsx": "^4.19.0" } } diff --git a/examples/nodejs/real-time-file-example.ts b/examples/nodejs/real-time-file-example.ts index c052e35..c0e35bb 100644 --- a/examples/nodejs/real-time-file-example.ts +++ b/examples/nodejs/real-time-file-example.ts @@ -1,33 +1,27 @@ +/** + * This file showcases the real-time-client package being used in NodeJS. + * + * It will connect to the real-time API and transcribe a file in real-time. + * To run this example, you will need to have a Speechmatics API key, + * which can be generated from the Speechmatics Portal: https://portal.speechmatics.com/api-keys + * + * NOTE: This script is run as an ES Module via tsx, letting us use top-level await. + * The library also works with CommonJS, but the code would need to be wrapped in an async function. + */ import { RealtimeClient } from '@speechmatics/real-time-client'; import fs from 'node:fs'; -import path from 'node:path'; import dotenv from 'dotenv'; +import { createSpeechmaticsJWT } from '@speechmatics/auth'; dotenv.config(); -const client = new RealtimeClient(); - -async function fetchJWT(): Promise { - const apiKey = process.env.API_KEY; - if (!apiKey) { - throw new Error('Please set API_KEY in .env file'); - } - const resp = await fetch('https://mp.speechmatics.com/v1/api_keys?type=rt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${process.env.API_KEY}`, - }, - body: JSON.stringify({ - ttl: 3600, - }), - }); - if (!resp.ok) { - throw new Error('Bad response from API', { cause: resp }); - } - return (await resp.json()).key_value; +const apiKey = process.env.API_KEY; +if (!apiKey) { + throw new Error('Please set the API_KEY environment variable'); } +const client = new RealtimeClient(); + let finalText = ''; client.addEventListener('receiveMessage', ({ data }) => { @@ -46,30 +40,29 @@ client.addEventListener('receiveMessage', ({ data }) => { } }); -(async () => { - const jwt = await fetchJWT(); +const jwt = await createSpeechmaticsJWT({ + type: 'rt', + apiKey, + ttl: 60, // 1 minute +}); - const fileStream = fs.createReadStream( - path.join(__dirname, './example.wav'), - { - highWaterMark: 4096, //avoid sending faster than realtime - }, - ); +const fileStream = fs.createReadStream('./example.wav', { + highWaterMark: 4096, //avoid sending faster than realtime +}); - await client.start(jwt, { - transcription_config: { - language: 'en', - enable_partials: true, - }, - }); +await client.start(jwt, { + transcription_config: { + language: 'en', + enable_partials: true, + }, +}); - //send it - fileStream.on('data', (sample) => { - client.sendAudio(sample); - }); +//send it +fileStream.on('data', (sample) => { + client.sendAudio(sample); +}); - //end the session - fileStream.on('end', () => { - client.stopRecognition(); - }); -})(); +//end the session +fileStream.on('end', () => { + client.stopRecognition(); +}); diff --git a/package.json b/package.json index e7726ae..229edb0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "clean:deps": "rm -rf node_modules && pnpm -r exec rm -rf node_modules", "clean:builds": "pnpm -r exec rm -rf dist", "clean": "pnpm clean:deps && pnpm clean:builds", - "format": "biome format --write .", "lint": "biome lint --write .", @@ -30,6 +29,7 @@ "rollup": "^4.21.2", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-esbuild": "^6.1.1", + "tsx": "^4.19.0", "typescript": "^5.5.4" } } diff --git a/packages/auth/README.md b/packages/auth/README.md new file mode 100644 index 0000000..0f7775d --- /dev/null +++ b/packages/auth/README.md @@ -0,0 +1,71 @@ +# Speechmatics authentication 🔑 + +Library for managing authentication with Speechmatics APIs + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [License](#license) + +## Installation + +```sh +npm i @speechmatics/auth +``` + +## Usage + +```typescript +import { RealtimeClient } from '@speechmatics/real-time-client'; +import { createSpeechmaticsJWT } from '@speechmatics/auth'; + +const client = new RealtimeClient(); + +client.addEventListener('receiveMessage', ({ data }) => { + // Handle transcription messages +}); + +async function transcribeFileRealtime () { + const jwt = await createSpeechmaticsJWT({ + type: 'rt', + apiKey, + ttl: 60, // 1 minute + }); + + const fileStream = fs.createReadStream( + path.join(__dirname, './example.wav'), + { + highWaterMark: 4096, //avoid sending faster than realtime + }, + ); + + await client.start(jwt, { + transcription_config: { + language: 'en', + enable_partials: true, + }, + }); + + //send it + fileStream.on('data', (sample) => { + client.sendAudio(sample); + }); + + //end the session + fileStream.on('end', () => { + client.stopRecognition(); + }); +} + +transcribeFileRealtime(); +``` + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request for any changes. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 0000000..0d63aed --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,25 @@ +{ + "name": "@speechmatics/auth", + "version": "0.0.1", + "description": "Library for fetching temporary keys for Speechmatics APIs", + "main": "dist/index.js", + "module": "dist/index.mjs", + "typings": "dist/index.d.ts", + "files": ["dist/", "README.md"], + "scripts": { + "build": "rm -rf dist && rollup -c", + "prepare": "pnpm build", + "test": "node --import tsx ./test/index.test.ts" + }, + "keywords": [ + "Speechmatics", + "authentication", + "transcription", + "batch", + "real-time" + ], + "license": "MIT", + "devDependencies": { + "jwt-decode": "^4.0.0" + } +} diff --git a/packages/auth/rollup.config.mjs b/packages/auth/rollup.config.mjs new file mode 100644 index 0000000..464ef40 --- /dev/null +++ b/packages/auth/rollup.config.mjs @@ -0,0 +1,50 @@ +import esbuild from 'rollup-plugin-esbuild'; +import dts from 'rollup-plugin-dts'; + +import packageJSON from './package.json' assert { type: 'json' }; +const name = packageJSON.main.replace(/\.js$/, ''); + +// Based on gist +//https://gist.github.com/aleclarson/9900ed2a9a3119d865286b218e14d226 + +/** @returns {import("rollup").RollupOptions[]} */ +export default function rollup() { + return [ + { + plugins: [ + esbuild({ + define: { + SDK_VERSION: `'${packageJSON.version}'`, + }, + }), + ], + input: 'src/index.ts', + output: [ + { + file: `${name}.js`, + format: 'cjs', + sourcemap: true, + }, + { + file: `${name}.mjs`, + format: 'es', + sourcemap: true, + }, + ], + }, + + { + plugins: [ + dts({ + compilerOptions: { + removeComments: true, + }, + }), + ], + input: 'src/index.ts', + output: { + file: `${name}.d.ts`, + }, + }, + ]; +} diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts new file mode 100644 index 0000000..ca59946 --- /dev/null +++ b/packages/auth/src/index.ts @@ -0,0 +1,84 @@ +// See documentation at https://docs.speechmatics.com/introduction/authentication#temporary-key-configuration +export async function createSpeechmaticsJWT({ + type, + apiKey, + clientRef, + ttl = 60, + managementPlatformURL = 'https://mp.speechmatics.com/v1', + region = 'eu', +}: { + type: 'batch' | 'rt' | 'flow'; + apiKey: string; + clientRef?: string; + ttl?: number; + region?: 'eu' | 'usa' | 'au'; + managementPlatformURL?: string; +}): Promise { + if (type === 'batch' && !clientRef) { + throw new SpeechmaticsJWTError( + 'ValidationFailed', + 'Must set the `client_ref` parameter when using temporary keys for batch transcription. See documentation at https://docs.speechmatics.com/introduction/authentication#batch-transcription', + ); + } + + const resp = await fetch(`${managementPlatformURL}/api_keys?type=${type}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + ttl, + region, + client_ref: clientRef, + }), + }); + + if (resp.ok) { + return (await resp.json()).key_value; + } + + // Handle errors + if (resp.status === 403) { + throw new SpeechmaticsJWTError('Unauthorized', 'unauthorized'); + } + + let json: { code: string; message: string }; + + try { + json = await resp.json(); + } catch (e) { + throw new SpeechmaticsJWTError( + 'UnknownError', + 'Failed to parse JSON response', + { cause: e }, + ); + } + + if (resp.status === 422) { + throw new SpeechmaticsJWTError('ValidationFailed', json.message, { + cause: json, + }); + } + + throw new SpeechmaticsJWTError( + 'UnknownError', + `Got response with status ${resp.status}`, + ); +} + +export type SpeechmaticsJWTErrorType = + | 'ValidationFailed' + | 'Unauthorized' + | 'UnknownError'; + +export class SpeechmaticsJWTError extends Error { + constructor( + readonly type: SpeechmaticsJWTErrorType, + message: string, + opts?: ErrorOptions, + ) { + super(message, opts); + this.name = 'SpeechmaticsJWTError'; + } +} diff --git a/packages/auth/test/index.test.ts b/packages/auth/test/index.test.ts new file mode 100644 index 0000000..537919c --- /dev/null +++ b/packages/auth/test/index.test.ts @@ -0,0 +1,45 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { createSpeechmaticsJWT } from '../src'; +import { jwtDecode } from 'jwt-decode'; + +// biome-ignore lint/style/noNonNullAssertion: +const apiKey = process.env.API_KEY!; + +test('Request valid JWT', async (t) => { + for (const type of ['batch', 'rt', 'flow'] as const) { + const ttl = 60; + const jwt = await createSpeechmaticsJWT({ + type, + ttl, + apiKey, + clientRef: 'test', + }); + const decoded = jwtDecode(jwt); + assert( + typeof decoded.exp === 'number' && + typeof decoded.iat === 'number' && + decoded.exp - decoded.iat === ttl, + ); + } +}); + +test('Request too short TTL', async () => { + for (const type of ['batch', 'rt', 'flow'] as const) { + const ttl = 30; + + assert.rejects( + createSpeechmaticsJWT({ + type, + ttl, + apiKey, + clientRef: 'test', + }), + { + name: 'SpeechmaticsJWTError', + type: 'ValidationFailed', + message: 'ttl in body should be greater than or equal to 60', + }, + ); + } +}); diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 0000000..4082f16 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/flow-client/package.json b/packages/flow-client/package.json index e8f3190..0b554cb 100644 --- a/packages/flow-client/package.json +++ b/packages/flow-client/package.json @@ -22,7 +22,6 @@ }, "devDependencies": { "@rollup/plugin-inject": "^5.0.5", - "@types/ws": "^8.5.12", - "tsx": "^4.19.0" + "@types/ws": "^8.5.12" } } diff --git a/packages/real-time-client/package.json b/packages/real-time-client/package.json index b25e346..2262215 100644 --- a/packages/real-time-client/package.json +++ b/packages/real-time-client/package.json @@ -29,7 +29,6 @@ }, "devDependencies": { "@rollup/plugin-inject": "^5.0.5", - "tsx": "^4.19.0", "yaml": "^2.5.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7be601d..7c5e412 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: rollup-plugin-esbuild: specifier: ^6.1.1 version: 6.1.1(esbuild@0.23.1)(rollup@4.21.2) + tsx: + specifier: ^4.19.0 + version: 4.19.0 typescript: specifier: ^5.5.4 version: 5.5.4 @@ -41,6 +44,9 @@ importers: '@picocss/pico': specifier: ^2.0.6 version: 2.0.6 + '@speechmatics/auth': + specifier: workspace:* + version: link:../../packages/auth '@speechmatics/browser-audio-input-react': specifier: workspace:* version: link:../../packages/browser-audio-input-react @@ -49,7 +55,7 @@ importers: version: link:../../packages/flow-client-react next: specifier: 15.0.1 - version: 15.0.1(react-dom@19.0.0-rc-69d4b800-20241021)(react@19.0.0-rc-69d4b800-20241021) + version: 15.0.1(react-dom@19.0.0-rc-69d4b800-20241021(react@19.0.0-rc-69d4b800-20241021))(react@19.0.0-rc-69d4b800-20241021) react: specifier: 19.0.0-rc-69d4b800-20241021 version: 19.0.0-rc-69d4b800-20241021 @@ -75,6 +81,9 @@ importers: examples/nodejs: dependencies: + '@speechmatics/auth': + specifier: workspace:* + version: link:../../packages/auth '@speechmatics/batch-client': specifier: workspace:* version: link:../../packages/batch-client @@ -84,10 +93,12 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 + + packages/auth: devDependencies: - tsx: - specifier: ^4.19.0 - version: 4.19.0 + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 packages/batch-client: dependencies: @@ -132,9 +143,6 @@ importers: '@types/ws': specifier: ^8.5.12 version: 8.5.12 - tsx: - specifier: ^4.19.0 - version: 4.19.0 packages/flow-client-react: dependencies: @@ -170,9 +178,6 @@ importers: '@rollup/plugin-inject': specifier: ^5.0.5 version: 5.0.5(rollup@4.21.2) - tsx: - specifier: ^4.19.0 - version: 4.19.0 yaml: specifier: ^2.5.0 version: 2.5.0 @@ -1221,6 +1226,10 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -2107,7 +2116,7 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@nestjs/axios@3.0.2(@nestjs/common@10.3.0)(axios@1.7.4)(rxjs@7.8.1)': + '@nestjs/axios@3.0.2(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(axios@1.7.4)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1) axios: 1.7.4 @@ -2121,7 +2130,7 @@ snapshots: tslib: 2.6.2 uid: 2.0.2 - '@nestjs/core@10.3.0(@nestjs/common@10.3.0)(reflect-metadata@0.1.13)(rxjs@7.8.1)': + '@nestjs/core@10.3.0(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(reflect-metadata@0.1.13)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 @@ -2183,9 +2192,9 @@ snapshots: '@openapitools/openapi-generator-cli@2.13.5': dependencies: - '@nestjs/axios': 3.0.2(@nestjs/common@10.3.0)(axios@1.7.4)(rxjs@7.8.1) + '@nestjs/axios': 3.0.2(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(axios@1.7.4)(rxjs@7.8.1) '@nestjs/common': 10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.3.0(@nestjs/common@10.3.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.3.0(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 axios: 1.7.4 chalk: 4.1.2 @@ -2218,6 +2227,7 @@ snapshots: '@rollup/pluginutils': 5.1.3(rollup@4.21.2) estree-walker: 2.0.2 magic-string: 0.30.12 + optionalDependencies: rollup: 4.21.2 '@rollup/pluginutils@5.1.3(rollup@4.21.2)': @@ -2225,6 +2235,7 @@ snapshots: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 + optionalDependencies: rollup: 4.21.2 '@rollup/rollup-android-arm-eabi@4.21.2': @@ -2743,6 +2754,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jwt-decode@4.0.0: {} + load-yaml-file@0.2.0: dependencies: graceful-fs: 4.2.11 @@ -2807,7 +2820,7 @@ snapshots: nanoid@3.3.7: {} - next@15.0.1(react-dom@19.0.0-rc-69d4b800-20241021)(react@19.0.0-rc-69d4b800-20241021): + next@15.0.1(react-dom@19.0.0-rc-69d4b800-20241021(react@19.0.0-rc-69d4b800-20241021))(react@19.0.0-rc-69d4b800-20241021): dependencies: '@next/env': 15.0.1 '@swc/counter': 0.1.3