@hushscript/sdk 0.0.0-stage → 1.0.1

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 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
- # Temporary Holding Version
1
+ # Hushscript TypeScript SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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