@hushscript/sdk 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +447 -2
- package/dist/index.d.ts +4366 -0
- package/dist/index.js +3811 -0
- package/package.json +72 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hushscript
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,448 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hushscript TypeScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official TypeScript SDK for [Hushscript](https://hushscript.com), a privacy-first, pay-as-you-go
|
|
4
|
+
transcription service. It uploads a file, waits for the transcript and hands it back, and it handles
|
|
5
|
+
the failures in between so you do not have to.
|
|
6
|
+
|
|
7
|
+
## Why Hushscript
|
|
8
|
+
|
|
9
|
+
- **No subscription.** You buy prepaid minute packs, and nothing recurs. Current prices are on the
|
|
10
|
+
[pricing page](https://hushscript.com/pricing).
|
|
11
|
+
- **The audio you send isn't kept.** The audio sent for transcription isn't kept after processing. Stored
|
|
12
|
+
transcripts are encrypted at rest, and you choose how long they stay.
|
|
13
|
+
- **Free speaker labels and AI insights.** Speaker labels carry no extra charge (medical mode and
|
|
14
|
+
translation cost more). The first all-in AI insights run on a transcript (summaries, action items,
|
|
15
|
+
quotes, chapters) is free in the Hushscript app.
|
|
16
|
+
- **Built for specialist recordings.** Key terms, prompts and multichannel audio in
|
|
17
|
+
around 99 languages, plus a medical mode for English, Spanish, French and German.
|
|
18
|
+
- **An SDK you can read.** It is MIT licensed, official, sends no telemetry, and is tested against a
|
|
19
|
+
[conformance suite](https://github.com/Hushscript/hushscript-sdk-conformance) shared with the other
|
|
20
|
+
Hushscript SDKs.
|
|
21
|
+
|
|
22
|
+
## What you can do with it
|
|
23
|
+
|
|
24
|
+
- Transcribe a file from a path, `Blob`, `Uint8Array` or stream, with progress callbacks and
|
|
25
|
+
retries that cannot charge you twice.
|
|
26
|
+
- Set the language or let it be detected, turn speaker labels on or off, and pass `keyterms` and a
|
|
27
|
+
`prompt` to steer the vocabulary.
|
|
28
|
+
- Use medical mode, multichannel audio, or translate into other languages (`translateTo`) in the
|
|
29
|
+
same call.
|
|
30
|
+
- Export SRT, WebVTT and plain text that match the Hushscript app's exports byte for byte, including
|
|
31
|
+
translations and the AI-cleaned body.
|
|
32
|
+
- Resume an interrupted job with its `resumeToken` instead of paying twice.
|
|
33
|
+
- Check your balance, list transcripts and jobs, and buy minute packs from code (off until you
|
|
34
|
+
allow it).
|
|
35
|
+
- Let an agent sign up, pay at a hosted checkout and claim its own token, with no human account.
|
|
36
|
+
|
|
37
|
+
[Website](https://hushscript.com) · [Pricing](https://hushscript.com/pricing) · [SDK docs](https://hushscript.com/sdks) · [API reference](https://hushscript.com/developers)
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install @hushscript/sdk
|
|
43
|
+
export HUSHSCRIPT_TOKEN=hsr1:... # Account → MCP in the Hushscript app
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { writeFile } from "node:fs/promises";
|
|
48
|
+
import { Hushscript } from "@hushscript/sdk";
|
|
49
|
+
|
|
50
|
+
const hs = new Hushscript(); // reads HUSHSCRIPT_TOKEN
|
|
51
|
+
const transcript = await hs.transcribe("meeting.mp3");
|
|
52
|
+
console.log(transcript.text);
|
|
53
|
+
await writeFile("meeting.srt", transcript.toSRT());
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Inputs
|
|
57
|
+
|
|
58
|
+
`transcribe()` and `resume()` take a path, a `Blob`, a `Uint8Array` or a `ReadableStream<Uint8Array>`.
|
|
59
|
+
A stream is read once, in order, one part at a time, so it needs its exact `size` and `durationSeconds`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const response = await fetch(url);
|
|
63
|
+
const transcript = await hs.transcribe(response.body!, {
|
|
64
|
+
size: Number(response.headers.get("content-length")),
|
|
65
|
+
durationSeconds: 1800,
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The stream stays yours: the SDK never cancels or closes it, on success, failure or abort. It only
|
|
70
|
+
releases its reader, so you can read what is left or cancel it yourself.
|
|
71
|
+
|
|
72
|
+
## Check my balance
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const balance = await hs.balance();
|
|
76
|
+
console.log(`${balance.minutes} minutes to spend`);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`balance.minutes` is what you can spend now. Minutes held for jobs that are still running are
|
|
80
|
+
already taken out of it, so do not subtract `heldSeconds` again.
|
|
81
|
+
|
|
82
|
+
## List my transcripts
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
for await (const transcript of hs.transcripts.list()) {
|
|
86
|
+
console.log(transcript.id, transcript.title);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The list holds finished transcripts only. A recording that is still processing is a job, so look for
|
|
91
|
+
it in `hs.jobs.list({ active: true })`. `hs.jobs.wait(id)` waits for one, and `hs.transcripts.get(id)`
|
|
92
|
+
returns the full `Transcript` once it is done. `hs.transcripts.page({ cursor })` fetches one page; a
|
|
93
|
+
page whose `next_cursor` is the cursor you sent raises `pagination_loop`. The iterator yields that page's
|
|
94
|
+
items first, then raises `pagination_loop`.
|
|
95
|
+
|
|
96
|
+
## Buy a pack
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
for (const pack of await hs.billing.packs()) {
|
|
100
|
+
console.log(pack.id, `${pack.minutes} minutes`, pack.price.formatted);
|
|
101
|
+
}
|
|
102
|
+
const purchase = await hs.buy("300min", { acknowledgeNoRefund: true });
|
|
103
|
+
console.log(`${purchase.minutes} minutes to spend now`);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The packs are `45min`, `300min`, `900min`, `1800min` and `6000min`. Prices depend on your country,
|
|
107
|
+
so read them from `pack.price` and never hard-code one.
|
|
108
|
+
|
|
109
|
+
`buy()` charges your saved default card, or the only card that can be charged when there is no
|
|
110
|
+
default, and returns a `Purchase`: the `seconds` and `minutes` from the purchase response, without a
|
|
111
|
+
follow-up request. A lost or unusable response can still raise an error after payment; retry with
|
|
112
|
+
the `idempotencyKey` attached to that error. With no usable card it
|
|
113
|
+
raises `PaymentMethodRequiredError` and sends nothing: `no_default_card` when there is none (add one
|
|
114
|
+
at the URL from `hs.billing.addCardUrl()` or in the app), `default_card_required` when several
|
|
115
|
+
cards could be charged and none is the default (pick one with `hs.billing.setDefaultCard(id)`).
|
|
116
|
+
|
|
117
|
+
Show your buyer this notice before you call it, then pass `acknowledgeNoRefund: true`:
|
|
118
|
+
|
|
119
|
+
> Minutes are added to your account instantly after payment. All purchases are final and
|
|
120
|
+
> non-refundable.
|
|
121
|
+
|
|
122
|
+
The flag records that the notice was shown. It does not gate the charge, and without it `buy()`
|
|
123
|
+
still goes ahead.
|
|
124
|
+
|
|
125
|
+
### Allow buying first
|
|
126
|
+
|
|
127
|
+
Buying from code is off until you allow it in three places. If any is missing, the call fails with
|
|
128
|
+
`pat_scope_missing`.
|
|
129
|
+
|
|
130
|
+
1. The token has the permission labeled "Buy minutes and manage your saved payment methods" (the
|
|
131
|
+
`billing` scope), ticked when you create it.
|
|
132
|
+
2. The token has "Let this token buy minutes" turned on, on the same "Create a new token" form.
|
|
133
|
+
3. "Let connected apps buy minutes" is on for the whole account.
|
|
134
|
+
|
|
135
|
+
All of it is in the app under Account → MCP.
|
|
136
|
+
|
|
137
|
+
### When a purchase does not go through
|
|
138
|
+
|
|
139
|
+
If the card is declined or cannot be charged without you, the call fails with
|
|
140
|
+
`pat_purchase_requires_app` (403); if the bank wants to confirm the payment, it fails with
|
|
141
|
+
`pat_purchase_requires_authentication` (403). In both cases nothing was charged: buy that pack in
|
|
142
|
+
the app.
|
|
143
|
+
|
|
144
|
+
A purchase is retried at most once, on a network error, a 502 or a 503, with the same idempotency
|
|
145
|
+
key, so those retries cannot charge you twice. When `buy()` still gives up after the request was
|
|
146
|
+
sent (the connection dropped twice, or the answer was not one the SDK can use), the card may have
|
|
147
|
+
been charged. A 401, 429, or any 403 other than the two refusals above counts too: after the retry
|
|
148
|
+
it can follow a first attempt that did charge. Every such error
|
|
149
|
+
carries `idempotencyKey`; ask again with it and the purchase can never happen twice:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { HushscriptError, type Purchase } from "@hushscript/sdk";
|
|
153
|
+
|
|
154
|
+
let purchase: Purchase;
|
|
155
|
+
try {
|
|
156
|
+
purchase = await hs.buy("300min", { acknowledgeNoRefund: true });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (!(error instanceof HushscriptError) || error.idempotencyKey === null) throw error;
|
|
159
|
+
// Only these refusals charged nothing. A 401, 429, or any 403 except 4021/4023 can follow a charged
|
|
160
|
+
// first attempt (the server answers them before replaying a purchase), so retry like a lost answer.
|
|
161
|
+
const refused =
|
|
162
|
+
error.code !== "bad_response" &&
|
|
163
|
+
((error.status !== null && [400, 402, 404, 422].includes(error.status)) ||
|
|
164
|
+
(error.status === 403 && (error.numericCode === 4021 || error.numericCode === 4023)));
|
|
165
|
+
if (refused) {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
purchase = await hs.buy("300min", {
|
|
169
|
+
acknowledgeNoRefund: true,
|
|
170
|
+
idempotencyKey: error.idempotencyKey,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A `buy()` that failed before any attempt was sent (for example the connection was refused, or the
|
|
176
|
+
SDK failed unexpectedly "before the request was sent") carries no `idempotencyKey`: nothing can have
|
|
177
|
+
been charged, so ask again as a new purchase.
|
|
178
|
+
|
|
179
|
+
A key you pass yourself must be 1 to 128 characters from `A-Za-z0-9_-`. Do not reuse a key for a
|
|
180
|
+
different pack.
|
|
181
|
+
|
|
182
|
+
## Export subtitles
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { writeFile } from "node:fs/promises";
|
|
186
|
+
|
|
187
|
+
await writeFile("meeting.srt", transcript.toSRT());
|
|
188
|
+
await writeFile("meeting.vtt", transcript.toVTT({ language: "es" }));
|
|
189
|
+
await writeFile("meeting.txt", transcript.toText({ timestamps: true }));
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The output is byte for byte what the Hushscript app exports. `toSRT` and `toVTT` wrap lines into
|
|
193
|
+
caption cues by default; pass `captions: false` for one cue per utterance.
|
|
194
|
+
|
|
195
|
+
## Agents: sign up without a human account
|
|
196
|
+
|
|
197
|
+
An agent can open an account, have it paid for at a hosted checkout page, and claim its own token.
|
|
198
|
+
None of these calls needs a token yet, so they are static.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { open } from "node:fs/promises";
|
|
202
|
+
|
|
203
|
+
// The server shows each token once, so write every one down the moment it arrives.
|
|
204
|
+
const keep = async (token: string): Promise<void> => {
|
|
205
|
+
// The mode only applies when the file is created, so tighten it before the token goes in.
|
|
206
|
+
const file = await open("agent-token", "w", 0o600);
|
|
207
|
+
try {
|
|
208
|
+
await file.chmod(0o600);
|
|
209
|
+
await file.writeFile(`${token}\n`);
|
|
210
|
+
} finally {
|
|
211
|
+
await file.close();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const signup = await Hushscript.agent.signup({
|
|
216
|
+
email: "agent@example.com",
|
|
217
|
+
pack: "45min",
|
|
218
|
+
acceptTerms: "<the policy version you accept>",
|
|
219
|
+
agent: { name: "My agent", platform: "custom" },
|
|
220
|
+
});
|
|
221
|
+
console.log(`Pay ${signup.price.formatted} at ${signup.checkoutUrl}`);
|
|
222
|
+
|
|
223
|
+
// After the checkout is paid:
|
|
224
|
+
const account = await Hushscript.agent.claim(signup.signupId, signup.claimSecret);
|
|
225
|
+
await keep(account.token); // before anything else can fail
|
|
226
|
+
const hs = new Hushscript({ token: account.token });
|
|
227
|
+
const next = await hs.agent.rotateToken(); // revokes the claimed token; the client uses `next.token`
|
|
228
|
+
await keep(next.token);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- `acceptTerms` is never filled in for you. A wrong version fails with `policy_version_stale`, and
|
|
232
|
+
the error's `hint` names the current one.
|
|
233
|
+
- The claim answers `wrong_state` (a `ConflictError`) until the checkout is paid.
|
|
234
|
+
- New agent accounts can buy only `45min` and `300min` until earlier purchases settle.
|
|
235
|
+
- Keep `claimSecret` and every token: the server shows each of them once, and a rotation revokes
|
|
236
|
+
the token it replaces, so lose the new one and the account is out of reach. A claim or a rotation
|
|
237
|
+
whose answer is lost, or comes back without a token, raises `OutcomeUnknownError` and is never
|
|
238
|
+
retried, because a retry could not succeed; its `hint` says what to check.
|
|
239
|
+
- `dryRun: true` validates a signup without opening a checkout.
|
|
240
|
+
|
|
241
|
+
## Errors
|
|
242
|
+
|
|
243
|
+
Every error the SDK raises extends `HushscriptError`, so one `catch` covers everything:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { HushscriptError, InsufficientBalanceError } from "@hushscript/sdk";
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await hs.transcribe("meeting.mp3");
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof InsufficientBalanceError) console.log(error.hint);
|
|
252
|
+
else if (error instanceof HushscriptError) console.log(error.code, error.hint ?? error.message);
|
|
253
|
+
else throw error;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
An error carries `code` (a slug such as `insufficient_balance`), `numericCode`, `status`, `message`,
|
|
258
|
+
`hint` (what to do next, or `null`), `details` and `retryAfter`. The classes:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
HushscriptError
|
|
262
|
+
├─ ConfigurationError
|
|
263
|
+
├─ PreflightError no credit was held
|
|
264
|
+
│ ├─ UnsupportedMediaError
|
|
265
|
+
│ ├─ FileLimitError
|
|
266
|
+
│ ├─ DailyLimitError
|
|
267
|
+
│ └─ ResumeMismatchError
|
|
268
|
+
├─ ApiError has status
|
|
269
|
+
│ ├─ AuthenticationError 401
|
|
270
|
+
│ ├─ PermissionDeniedError 403
|
|
271
|
+
│ ├─ InsufficientBalanceError 402
|
|
272
|
+
│ ├─ NotFoundError 404
|
|
273
|
+
│ ├─ ConflictError 409
|
|
274
|
+
│ ├─ ValidationError 400, 422
|
|
275
|
+
│ └─ RateLimitError 429
|
|
276
|
+
├─ PaymentMethodRequiredError
|
|
277
|
+
├─ UploadError
|
|
278
|
+
├─ JobFailedError
|
|
279
|
+
├─ WaitTimeoutError has resumeToken
|
|
280
|
+
├─ AbortError has resumeToken once a job exists
|
|
281
|
+
├─ NetworkError
|
|
282
|
+
└─ OutcomeUnknownError
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Every error that wraps another one names the original's type and message in parentheses in its own
|
|
286
|
+
message, with secrets redacted, and keeps the original as the `cause`: a failed request
|
|
287
|
+
(`NetworkError`, `OutcomeUnknownError`, for example `(TypeError: fetch failed; code ECONNREFUSED)`),
|
|
288
|
+
an unreadable input (`file_unreadable`, `source_read_failed`), an unreadable duration
|
|
289
|
+
(`duration_unreadable`) and an unusable response (`bad_response`).
|
|
290
|
+
|
|
291
|
+
Only a `TypeError` (what `fetch` throws for a failed connection or body) is a network failure. Any
|
|
292
|
+
other value `fetch` throws (a `RangeError`, a string, an error of your own class) raises
|
|
293
|
+
`HushscriptError` with code `unexpected_error`, is not retried, and its message names the type, for
|
|
294
|
+
example `The SDK failed unexpectedly after the request was sent (RangeError: ...)`. After a claim or a
|
|
295
|
+
rotation went out it is an `OutcomeUnknownError`. The message says "before" or "after the request was
|
|
296
|
+
sent" only where the SDK knows, and is silent about it where it does not.
|
|
297
|
+
|
|
298
|
+
An error raised after an upload started, or after a purchase may have been sent, carries `idempotencyKey`;
|
|
299
|
+
once the job exists it carries `resumeToken` too. Pass the token to `hs.resume(token, file)` to carry on without a second charge.
|
|
300
|
+
|
|
301
|
+
## What the SDK does for you
|
|
302
|
+
|
|
303
|
+
- **Checks before it spends.** It sniffs the format from the file's bytes, probes the duration,
|
|
304
|
+
and refuses a file the server would refuse, or one your daily limit cannot cover, before any
|
|
305
|
+
minutes are held. It also checks `keyterms` and `prompt` against the server's limits: at most 1000
|
|
306
|
+
unique terms of at most 6 words and 128 characters each, and a prompt of at most 2000 characters
|
|
307
|
+
(with key terms, counted together as the server compiles them). Terms and prompt are trimmed,
|
|
308
|
+
and blanks and empty lists are not sent. Whether your balance covers the file is the server's call: a 402 comes back
|
|
309
|
+
when the upload is created, before any part is sent.
|
|
310
|
+
- **Keeps retries from charging twice.** Every upload and purchase carries an idempotency key, and
|
|
311
|
+
retries reuse it. A `resumeToken` replays the same job instead of starting a new one, and the key
|
|
312
|
+
on a failed purchase does the same for `buy()`.
|
|
313
|
+
- **Retries what is safe.** Reads, heartbeats and part uploads retry on network errors, 429 and 5xx
|
|
314
|
+
with jittered backoff, and honor `Retry-After` up to 60 seconds. A purchase retries once on a
|
|
315
|
+
network error, 502 or 503. A claim, a rotation and a signup never retry.
|
|
316
|
+
- **Paces uploads.** The API allows four upload creates a minute. A loop over a folder waits as
|
|
317
|
+
needed, and `onProgress` keeps reporting.
|
|
318
|
+
- **Keeps the upload alive.** It uploads parts in parallel with bounded memory, falls back to the
|
|
319
|
+
proxied route when direct upload fails, refreshes expired part URLs and sends a heartbeat every two
|
|
320
|
+
minutes.
|
|
321
|
+
- **Waits and fetches.** It polls the job until it is done, raises `JobFailedError` if it fails,
|
|
322
|
+
and gives you a `resumeToken` if the wait times out. The job keeps running.
|
|
323
|
+
- **Redacts the token and the claim secret** from every error, log line and `toString`, and the query
|
|
324
|
+
string of any URL in an error message, hint or cause (`https://host/path?[redacted]`, so a presigned
|
|
325
|
+
URL's signature never prints while the host and path still say what failed). The results that hand you a
|
|
326
|
+
secret (`AgentSignup.claimSecret`, `AgentClaim.token`, `RotatedToken.token`) keep it readable as a
|
|
327
|
+
field, but `console.log`, `util.inspect`, `String()` and template strings show `[redacted]` in its
|
|
328
|
+
place; `JSON.stringify` is your own export and carries the value.
|
|
329
|
+
|
|
330
|
+
- **Keeps what it does not know.** A field the API adds to a result later (`Account`, `Balance`,
|
|
331
|
+
`Job`, `Pack`, `Cards`, `Card`, `Purchase`, `Transaction`, `LedgerEntry`, `Hold`,
|
|
332
|
+
`TranscriptSummary`, `DeletedTranscript`, the agent signup, claim and rotation results, and
|
|
333
|
+
utterances) is kept on the object at run time, camelCased and nested objects included. The
|
|
334
|
+
fields the SDK maps itself always win, and the few fields it leaves out on purpose (a job's
|
|
335
|
+
private-result and fetch fields) stay out; `hs.api` returns the raw body.
|
|
336
|
+
|
|
337
|
+
## Supported formats
|
|
338
|
+
|
|
339
|
+
MP3, M4A, MP4 and MOV (the audio track of a video is used), WAV, FLAC, and OGG with Vorbis or Opus.
|
|
340
|
+
Files can be up to 5 GB and from 15 seconds to 10 hours. The SDK does not convert media. For
|
|
341
|
+
anything else, convert it first:
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
ffmpeg -i input -vn -c:a libopus output.opus
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Configuration
|
|
348
|
+
|
|
349
|
+
| Option | Env var | Default |
|
|
350
|
+
| ------------------------- | --------------------- | -------------------------------------------------------------------- |
|
|
351
|
+
| `token` | `HUSHSCRIPT_TOKEN` | none |
|
|
352
|
+
| `baseUrl` | `HUSHSCRIPT_BASE_URL` | `https://api.hushscript.com` |
|
|
353
|
+
| `timeout` | none | 60 s per request, at most 3600 |
|
|
354
|
+
| `maxRetries` | none | 3 |
|
|
355
|
+
| `logLevel` | `HUSHSCRIPT_LOG` | `off` (`debug` logs one metadata line per request to standard error) |
|
|
356
|
+
| `clock` | none | the system clock; pass a `Clock` in tests |
|
|
357
|
+
| `dangerouslyAllowBrowser` | none | `false`; for tests only |
|
|
358
|
+
|
|
359
|
+
An explicit option beats its environment variable. A `null`, empty or whitespace-only option or
|
|
360
|
+
variable counts as not set: the SDK falls back to the environment variable, then to the default (a
|
|
361
|
+
token has no default, so it raises the missing-token `ConfigurationError`). The base URL must be `https://`, or `http://` to
|
|
362
|
+
`localhost`, `127.0.0.1` or `[::1]`, with no path. Each upload part has its own 300 s timeout, which you cannot
|
|
363
|
+
change.
|
|
364
|
+
|
|
365
|
+
## Privacy
|
|
366
|
+
|
|
367
|
+
The SDK sends no telemetry. The only client identification is the `User-Agent` header
|
|
368
|
+
(`hushscript-typescript/<version> <runtime>/<version>`). Logging is off by default;
|
|
369
|
+
`HUSHSCRIPT_LOG=debug` (or `logLevel: "debug"`) writes one line per request with the method, path,
|
|
370
|
+
status and timing, and never a header, body, title or transcript text.
|
|
371
|
+
|
|
372
|
+
## The low-level API
|
|
373
|
+
|
|
374
|
+
Every `/v1` operation is available as `hs.api`, typed from the OpenAPI document and sent through
|
|
375
|
+
the same retries, timeouts and redaction:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const { data } = await hs.api.GET("/v1/account");
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The generated `paths`, `components` and `operations` types are exported too.
|
|
382
|
+
|
|
383
|
+
## Server only
|
|
384
|
+
|
|
385
|
+
The SDK is for servers. The API does not accept browser origins, and a token in a browser bundle is
|
|
386
|
+
a leaked token, so constructing a client where `window` and `document` exist throws
|
|
387
|
+
`ConfigurationError`.
|
|
388
|
+
|
|
389
|
+
## Pricing
|
|
390
|
+
|
|
391
|
+
Personal accounts buy prepaid minute packs. The price of a pack depends on your
|
|
392
|
+
country, and your minutes stay valid for a year after your last transcription or purchase. A
|
|
393
|
+
transcription uses the minutes of the recording; medical mode and each translation target use more.
|
|
394
|
+
Current prices are on [hushscript.com/pricing](https://hushscript.com/pricing).
|
|
395
|
+
|
|
396
|
+
## Requirements
|
|
397
|
+
|
|
398
|
+
Node 22 or newer; CI tests Node 22, 24, and 26. Bun 1.2+ and Deno 2+ are checked for package
|
|
399
|
+
imports and a loopback HTTP balance request. HTTP/2 stream-reset and replay behavior in those
|
|
400
|
+
runtimes is not tested. The SDK is for server runtimes only: a token in a browser bundle is a leaked token.
|
|
401
|
+
|
|
402
|
+
The package is ESM-first: `import { ... } from "@hushscript/sdk"`. On Node 22.12 and newer,
|
|
403
|
+
`require("@hushscript/sdk")` also works, because Node can `require()` an ES module (the `default`
|
|
404
|
+
export condition points at the same ESM build). TypeScript consumers need TypeScript 5.5 or newer;
|
|
405
|
+
`just build` type-checks the packed declarations with exactly 5.5 in a container.
|
|
406
|
+
|
|
407
|
+
## Links
|
|
408
|
+
|
|
409
|
+
- Documentation: <https://hushscript.com/sdks>
|
|
410
|
+
- REST API reference: <https://hushscript.com/developers>
|
|
411
|
+
- Package on npm: <https://www.npmjs.com/package/@hushscript/sdk>
|
|
412
|
+
- Source: <https://github.com/Hushscript/hushscript-sdk-typescript>
|
|
413
|
+
- Other Hushscript SDKs: [Python](https://github.com/Hushscript/hushscript-sdk-python), [Go](https://github.com/Hushscript/hushscript-sdk-go), [PHP](https://github.com/Hushscript/hushscript-sdk-php), [CLI](https://github.com/Hushscript/hushscript-cli)
|
|
414
|
+
|
|
415
|
+
## Releases
|
|
416
|
+
|
|
417
|
+
Releases come from release-please. The package is published to npm by `publish.yml` through npm
|
|
418
|
+
trusted publishing (OIDC): no npm token is stored in GitHub. The very first publish of
|
|
419
|
+
`@hushscript/sdk` cannot use a trusted publisher, because the package does not exist on the registry
|
|
420
|
+
yet, so the maintainers publish it once by hand and then attach the trusted publisher (this
|
|
421
|
+
repository, workflow `publish.yml`, environment `release`) in the package settings on npmjs.com. Every later release is published by the workflow with provenance.
|
|
422
|
+
|
|
423
|
+
## Development
|
|
424
|
+
|
|
425
|
+
Everything runs in containers; nothing touches a host toolchain. You need Docker with Compose v2 and
|
|
426
|
+
[just](https://github.com/casey/just).
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
just ci # lint, then test and conformance on Node 22, 24, and 26, then examples and build
|
|
430
|
+
just test 24 # unit tests with coverage on one runtime
|
|
431
|
+
just lint # every static check
|
|
432
|
+
just regen # regenerate openapi.json and src/generated from the production spec
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The conformance mock is built from the `hushscript-sdk-conformance` repository at the tag or commit pinned in
|
|
436
|
+
`package.json` (`config.conformanceRef`). To use a local checkout instead:
|
|
437
|
+
|
|
438
|
+
```sh
|
|
439
|
+
CONFORMANCE_CONTEXT=../hushscript-sdk-conformance CONFORMANCE_DIR=../hushscript-sdk-conformance just ci
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Security
|
|
443
|
+
|
|
444
|
+
See [SECURITY.md](SECURITY.md).
|
|
445
|
+
|
|
446
|
+
## License
|
|
447
|
+
|
|
448
|
+
MIT
|