@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/dist/index.js ADDED
@@ -0,0 +1,3811 @@
1
+ import { inspect } from 'util';
2
+ import createClient from 'openapi-fetch';
3
+ import { setImmediate } from 'timers/promises';
4
+ import { parseBlob } from 'music-metadata';
5
+ import { randomUUID } from 'crypto';
6
+ import { stat, open } from 'fs/promises';
7
+
8
+ // src/client.ts
9
+
10
+ // package.json
11
+ var package_default = {
12
+ version: "1.0.0"};
13
+
14
+ // src/clock.ts
15
+ var systemClock = {
16
+ now: () => performance.now(),
17
+ sleep(ms, signal) {
18
+ return new Promise((resolve, reject) => {
19
+ if (signal?.aborted === true) {
20
+ reject(signal.reason);
21
+ return;
22
+ }
23
+ const onAbort = () => {
24
+ clearTimeout(timer);
25
+ reject(signal?.reason);
26
+ };
27
+ const timer = setTimeout(() => {
28
+ signal?.removeEventListener("abort", onAbort);
29
+ resolve();
30
+ }, ms);
31
+ signal?.addEventListener("abort", onAbort, { once: true });
32
+ });
33
+ }
34
+ };
35
+
36
+ // src/camel.ts
37
+ function camelKey(key) {
38
+ return key.replace(/_([a-z0-9])/g, (_whole, char) => char.toUpperCase());
39
+ }
40
+ function camelize(value) {
41
+ if (Array.isArray(value)) return value.map(camelize);
42
+ if (typeof value === "object" && value !== null) {
43
+ return Object.fromEntries(
44
+ Object.entries(value).map(([key, item]) => [camelKey(key), camelize(item)])
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+ function unknownFields(raw, known) {
50
+ const extra = Object.entries(raw).filter(([key]) => !known.has(key));
51
+ return camelize(Object.fromEntries(extra));
52
+ }
53
+ function camelizeKnown(raw, known) {
54
+ const own = Object.entries(raw).filter(([key]) => known.has(key));
55
+ return { ...unknownFields(raw, known), ...camelize(Object.fromEntries(own)) };
56
+ }
57
+
58
+ // src/generated/catalog.ts
59
+ var ERROR_CATALOG = {
60
+ "bad_request": { code: 1e3, status: 400, meaning: "The request body could not be parsed, or a required field was missing. Check the request against this operation's schema and resend." },
61
+ "invalid_email": { code: 1001, status: 400, meaning: "The email address failed format or safety validation (address lists, spoofed display names, control characters and quoted local parts are all rejected). Send a single plain address." },
62
+ "invalid_password": { code: 1002, status: 400, meaning: "The password did not meet the length bounds this API enforces. Choose a password between the documented minimum and maximum byte length." },
63
+ "missing_token": { code: 1003, status: 400, meaning: "No longer emitted: GET /api/auth/verify without a token now redirects to the app with ?error=link_expired instead of returning this JSON error. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it." },
64
+ "invalid_locale": { code: 1004, status: 400, meaning: "The requested locale is not one this account or API supports. Use one of the documented locale codes." },
65
+ "bad_part_number": { code: 1005, status: 400, meaning: "The part number is outside the plan for this upload (below 1 or past the last part for the declared size). Recompute the part count from the declared size and part size." },
66
+ "no_payment_method": { code: 1006, status: 400, meaning: "No payment method was available to complete this action. Add or select a saved payment method before retrying." },
67
+ "malformed_event": { code: 1007, status: 400, meaning: "An inbound webhook or event payload could not be parsed or verified. This is not actionable from a normal API client." },
68
+ "invalid_filename": { code: 1020, status: 422, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The uploaded file's name failed validation. Rename the file to remove unsupported characters and retry." },
69
+ "duration_out_of_range": { code: 1021, status: 422, meaning: "The declared or verified duration is shorter than 15 seconds or longer than this API's maximum accepted length of 10 hours. Trim or split the recording before uploading." },
70
+ "size_out_of_range": { code: 1022, status: 422, meaning: "The declared byte size is zero or exceeds this API's maximum accepted upload size. Compress or split the file before uploading." },
71
+ "part_too_large": { code: 1023, status: 422, meaning: "One uploaded part exceeded the maximum part size this API accepts. Resend that part split into smaller chunks." },
72
+ "size_exceeded": { code: 1024, status: 422, meaning: "The bytes received for this upload would exceed the size declared at creation. Start a new upload with the correct declared size." },
73
+ "no_parts": { code: 1025, status: 422, meaning: "The upload was marked complete with no parts recorded. Upload at least one part before calling complete." },
74
+ "invalid_title": { code: 1026, status: 422, meaning: "The provided title exceeded the maximum length or contained disallowed content. Shorten or sanitize the title and retry." },
75
+ "invalid_speaker_names": { code: 1027, status: 422, meaning: "The speaker-name mapping was not valid JSON or exceeded the allowed shape. Send a flat object of speaker label to display name." },
76
+ "too_many_names": { code: 1028, status: 422, meaning: "The speaker-name mapping named more speakers than this API allows on one transcript. Remove entries before retrying." },
77
+ "invalid_clip": { code: 1029, status: 422, meaning: "The preview clip could not be read as audio. Send a supported audio format within the preview size and duration limits." },
78
+ "clip_too_long": { code: 1030, status: 422, meaning: "The preview clip exceeds the maximum preview duration. Trim the clip and retry." },
79
+ "unsupported_format": { code: 1031, status: 422, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The audio container or codec could not be probed by this API. Re-encode to one of the documented supported formats." },
80
+ "unknown_pack": { code: 1032, status: 422, meaning: "The requested pack id does not exist in this account's catalogue. Read GET /v1/billing/packs for the current, valid pack ids." },
81
+ "invalid_transcription_options": { code: 1033, status: 422, meaning: "The transcription_options object failed validation (an unsupported language, an invalid combination of options, or an over-limit field). Re-check each option against its documented bounds." },
82
+ "invalid_edit": { code: 1034, status: 422, meaning: "The submitted transcript edit was not valid for the current body (bad utterance shape or an out-of-range index). Reload the current body before editing." },
83
+ "too_many_tags": { code: 1035, status: 422, meaning: "The request would leave more tags on the transcript than this API allows. Remove existing tags before adding new ones." },
84
+ "no_speech": { code: 1036, status: 422, meaning: "No speech was detected in the submitted audio. Confirm the source file actually contains an audio track with speech." },
85
+ "url_invalid": { code: 1037, status: 400, meaning: "The supplied source URL is not a well-formed https URL. Provide a direct https link to the audio or video file." },
86
+ "url_blocked": { code: 1038, status: 422, meaning: "The supplied URL resolves to a host or address this API refuses to fetch (internal ranges, this API's own storage, or a disallowed host). Use a public, third-party hosted URL instead." },
87
+ "url_needs_app": { code: 1039, status: 422, meaning: "This URL source needs interactive authentication this API cannot perform on the caller's behalf. Download the file yourself and upload the bytes directly instead." },
88
+ "invalid_idempotency_key": { code: 1040, status: 422, meaning: "The supplied idempotency_key was outside the accepted length bounds. Send a key between the documented minimum and maximum length, or omit it." },
89
+ "upload_incomplete": { code: 1041, status: 422, meaning: "The assembled object was shorter than the size declared at creation, so the hold was released and nothing was billed. Re-upload every part and call complete again." },
90
+ "url_target_busy": { code: 1042, status: 429, meaning: "This API is already fetching from that target host as fast as it will, so the fetch was refused before it was attempted. This bounds the target site, not the caller: a different source URL will go through immediately, and the same one will after a short wait." },
91
+ "policy_version_stale": { code: 1043, status: 422, meaning: "accept_terms_version did not match the server's current policy version. The server records its own value, never the caller's, so retry with the current version returned by this same error." },
92
+ "unauthorized": { code: 2e3, status: 401, meaning: "The bearer token is missing, malformed, expired, or revoked. This is also the answer for a revoked, malformed, or unknown Personal Access Token, so a caller holding a PAT should mint a fresh one rather than retry the same one. Obtain a fresh session token or Personal Access Token and retry with it in the Authorization header." },
93
+ "invalid_credentials": { code: 2001, status: 401, meaning: "The submitted email/password pair did not match an account. Check the credentials or use the password-reset flow." },
94
+ "turnstile_failed": { code: 2010, status: 403, meaning: "The Cloudflare Turnstile challenge could not be verified. Re-run the challenge on the client and resend with a fresh token." },
95
+ "country_restricted": { code: 2011, status: 403, meaning: "This action is not available from the caller's detected jurisdiction. This is a policy decision, not a transient failure; retrying will not help." },
96
+ "signup_not_allowed": { code: 2012, status: 403, meaning: "New account creation is currently closed for this signup path. Retry later or use an already-provisioned account." },
97
+ "account_suspended": { code: 2013, status: 403, meaning: "The account is suspended, most commonly due to an open payment dispute. Resolve the dispute or contact support before retrying." },
98
+ "bad_signature": { code: 2020, status: 400, meaning: "A cryptographic signature on this request could not be verified. Not reachable from this public API; indicates a caller configuration bug elsewhere in the stack." },
99
+ "org_required": { code: 2021, status: 403, meaning: "This action requires the caller to belong to an organization, and the bearer does not. Join or create an organization first. PATCH /api/org also answers it, writing nothing, when the patch names a field whose right the caller lacks: the billing contact, tax identity and auto_renew need the billing right, every other field the admin right. POST /api/org/invites and PATCH /api/org/members/:user_id answer it too when they hand out or take away the billing right (an invite carrying an explicit is_billing true; a patch naming is_billing) for a caller who does not hold it, except that while no member of the workspace holds the right any admin may grant it (a revoke always needs it). A workspace member removal answers it too when the target holds the billing right and the caller neither holds it nor is that member (a member may always leave; removing a holder needs the right, so the recovery rule above is reachable only after the last holder leaves, revokes their own right, or deletes their account). An invite that leaves is_billing out defaults it to the caller's own right for role admin, so an admin without the billing right inviting an admin sends a plain admin invite and is not refused." },
100
+ "pat_forbidden": { code: 2022, status: 403, meaning: "This route is not in the Personal Access Token scope map at all, so no token, regardless of scope, can reach it. Use a session bearer for this call instead." },
101
+ "pat_scope_missing": { code: 2023, status: 403, meaning: "The Personal Access Token does not carry the scope this route requires (see x-required-scope on the operation). Mint or use a token that includes it." },
102
+ "step_up_required": { code: 2024, status: 403, meaning: "Deleting an account needs proof it is the holder asking. DELETE /api/me carried neither current_password nor email_code, or sent current_password for a password set or changed in the last 7 days, or one the requesting session itself set. Send the current password (set more than 7 days ago, not by this session), or a code sent by POST /api/me/deletion-code." },
103
+ "card_required": { code: 3e3, status: 402, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: This purchase needs a card on file and the account has none. Add a card through the account's payment settings before retrying." },
104
+ "insufficient_balance": { code: 3001, status: 402, meaning: "The account's balance cannot cover the requested seconds. The error body's needed_seconds and balance_seconds report the shortfall; top up before retrying." },
105
+ "card_declined": { code: 3002, status: 402, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The card on file was declined when charged. Use a different payment method or contact the card issuer." },
106
+ "org_insufficient_balance": { code: 3003, status: 402, meaning: "The organization's shared minute pool cannot cover the requested seconds. The error body's org_balance_seconds reports the pool's current balance; an org admin must top it up." },
107
+ "not_found": { code: 4e3, status: 404, meaning: "No resource exists at this id for the authenticated caller. Confirm the id and that it belongs to this account." },
108
+ "wrong_state": { code: 4001, status: 409, meaning: "The target resource is not in a state this action allows (for example, an upload that already finished). Read its current state before retrying." },
109
+ "email_exists": { code: 4002, status: 409, meaning: "An account already exists for this email address. Sign in instead of registering again." },
110
+ "account_has_active_jobs": { code: 4003, status: 409, meaning: "This action cannot proceed while the account has active transcription jobs. Wait for them to finish, or cancel them, then retry." },
111
+ "ai_already_generated": { code: 4004, status: 409, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: An Insights bundle already exists for this transcript and this call did not ask for a paid regeneration. Pass a mode to request one." },
112
+ "promo_already_redeemed": { code: 4005, status: 409, meaning: "This promo code has already been redeemed on this account. Each code may be redeemed once per account." },
113
+ "org_already_member": { code: 4006, status: 409, meaning: "Creating an organization: the caller founded another organization that is still unpaid and either is still a member of it or created it less than 7 days ago (one unpaid founded organization at a time; leaving it does not lift the bound inside the 7 days and does after them; belonging to other organizations does not block it). Accepting an invite: the account is already a member of that organization." },
114
+ "org_plan_inactive": { code: 4007, status: 409, meaning: "The organization has no active plan, so its pool cannot fund this action. An org admin must renew the plan." },
115
+ "org_seat_limit": { code: 4008, status: 409, meaning: "The organization is already at its seat limit. Remove a member or increase the plan's seat count before inviting another." },
116
+ "org_wrong_region": { code: 4009, status: 409, meaning: "The target account's data region does not match the organization's region. Cross-region membership is not supported; the user needs an account in the org's region." },
117
+ "link_expired": { code: 4010, status: 410, meaning: "The magic link or similar time-boxed link has expired. Request a new one." },
118
+ "expired": { code: 4011, status: 410, meaning: "The referenced token or session has expired. Request a fresh one and retry." },
119
+ "invalid_code": { code: 4012, status: 410, meaning: "The submitted one-time code does not match what was issued. Re-check the code or request a new one." },
120
+ "no_pending": { code: 4013, status: 404, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: There is no pending operation of this kind to act on (for example, no pending export). Start the operation before trying to read its result." },
121
+ "promo_unavailable": { code: 4014, status: 410, meaning: "This promo code is not currently redeemable (expired, not yet active, or exhausted). Check the promotion's terms." },
122
+ "promo_card_required": { code: 4015, status: 409, meaning: "This promo requires a verified card on file before it can be redeemed. Add a card first." },
123
+ "invoice_not_found": { code: 4016, status: 404, meaning: "No invoice exists at this id for the caller's organization. Confirm the invoice id from GET /api/org/invoices." },
124
+ "org_retention_too_loose": { code: 4017, status: 409, meaning: "The requested retention setting exceeds the organization's configured maximum. Request a value at or below the org's ceiling." },
125
+ "org_plan_invoice_open": { code: 4018, status: 409, meaning: "The organization already has an open, unpaid plan invoice. Settle or wait for it to be superseded before requesting another." },
126
+ "pat_exchange_invalid": { code: 4019, status: 410, meaning: "The OAuth handoff exchange code is invalid, already used, or expired. Restart the handoff flow to obtain a new code." },
127
+ "pat_invalid": { code: 4020, status: 422, meaning: "The POST /api/pats request body failed validation: an empty or oversized name, an unknown or empty scope list, created_via outside app or oauth, or an invalid client_label. Fix the request body and retry. A bearer token that is itself malformed, revoked, or unknown never reaches this code; that case answers unauthorized (2000) instead." },
128
+ "pat_purchase_requires_app": { code: 4021, status: 403, meaning: "This purchase needs full Checkout, which this API cannot open or let the caller complete. Either the request had no quick_payment_method_id, in which case resend with one set to a card already on file (see GET /v1/billing/cards), or the saved card will not go through as attempted (declined, needs a different payment method, or was canceled), in which case retrying the same card changes nothing: fix or replace it, then retry with a brand new idempotency_key, or complete the purchase from the Hushscript app. A card whose issuer wants an interactive challenge is not a decline and answers 4023 instead." },
129
+ "pat_limit": { code: 4022, status: 409, meaning: "The account already holds the maximum number of Personal Access Tokens. Revoke an unused token before minting another." },
130
+ "pat_purchase_requires_authentication": { code: 4023, status: 403, meaning: "The saved card is fine, but the issuer wants a challenge (3DS) before this charge can go through. This API cannot open or let the caller complete that challenge, so the purchase is refused rather than left pending. Complete the purchase from the Hushscript app instead, where the holder can go through authentication." },
131
+ "org_signups_closed": { code: 4024, status: 403, meaning: "Creating a new organization is closed in this environment. Signups are open in every environment today, so this answers only where they were deliberately closed again. Existing organizations keep working normally; every other org route, including renewals, pack purchases, invites, and admin reads, stays open." },
132
+ "org_plan_frozen": { code: 4025, status: 402, meaning: "The organization's plan lapsed and its 30 day grace window has also passed, so no new organization funded upload is admitted. A job already running still settles normally. Paying a plan renewal clears the freeze right away." },
133
+ "org_pool_expired": { code: 4026, status: 402, meaning: "The organization's shared minute pool is present but its 365-day rolling clock has lapsed with no purchase, grant, or completed job to renew it. A pack purchase, an included plan grant, or completing another org-funded job all restart the clock for another year." },
134
+ "org_member_cap_exceeded": { code: 4027, status: 402, meaning: "This pool member has used their whole monthly share of the workspace's pooled seconds. The workspace pool itself may still have a balance; this is the per-member cap, separate from a plain insufficient-balance refusal." },
135
+ "org_renewal_payment_failed": { code: 4028, status: 402, meaning: "The daily renewal lane charged the organization's stored renewal card and Stripe answered with a structured failure. The invoice stays issued, the failure count goes up by one, a renewal-failed email carrying a fresh Checkout link for that same invoice is sent (flagged as the final attempt once the count reaches the cap of 3), and the lapsed term falls into the ordinary grace window. Once the cap is reached the lane stops retrying this organization automatically -- no further charge attempts, no further emails -- until the open invoice is paid by hand, which resets the count. GET /api/org exposes this as a derived renewal_capped boolean." },
136
+ "org_no_default_payment_method": { code: 4029, status: 409, meaning: "The chosen payment method is not one of the caller's own saved, fast checkout eligible cards, the caller has no Stripe customer at all, or auto renew was switched on with no renewal card stored yet. All three answer identically so this route can never be used to learn whether a given payment method id exists." },
137
+ "org_refund_window_closed": { code: 4030, status: 409, meaning: "The operator's signed fee-refund command came more than 30 days after the workspace's plan was first paid for, so the plan fee refund window has closed. No customer route returns this code; the fee refund exists only as that operator command. Packs are never refundable and never reach this check." },
138
+ "org_refund_not_eligible": { code: 4031, status: 409, meaning: "The target invoice of the operator's signed fee-refund command is not a paid, unrefunded plan invoice of this workspace, or the workspace already took its one refund. No customer route returns this code; the fee refund exists only as that operator command. Packs are never refundable regardless of this check." },
139
+ "org_refund_currency_mismatch": { code: 4032, status: 409, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. The signed admin fee-refund command answers its own currency_mismatch refusal instead. It used to mean: The Stripe charge settled in a currency different from the invoice's currency, so a partial refund amount cannot be computed. Contact support for a manual refund." },
140
+ "org_tax_id_invalid": { code: 4033, status: 400, meaning: "The tax id or its label does not match a shape this system accepts. This is a shape check only, never a registry lookup against a tax authority." },
141
+ "agent_signup_pending": { code: 4035, status: 409, meaning: "A signup already exists for this contact_email and has not yet been claimed, refunded, or expired. Wait for it to resolve or use a different contact address." },
142
+ "agent_claim_invalid": { code: 4036, status: 401, meaning: "The claim secret did not match, or the signup does not exist, has already been claimed, was refunded, or has expired. These cases answer identically on purpose. A confirmed-but-unpaid Checkout Session answers wrong_state (4001) instead once the secret itself has checked out." },
143
+ "agent_pack_locked": { code: 4037, status: 403, meaning: "An agent account's pack tier does not allow the requested pack yet. A fresh agent account is limited to the small packs; a larger pack unlocks only once enough prior payments have settled past the dispute window. Reached on POST /v1/agent/accounts (signup, always tier 0) and POST /v1/billing/purchase for an agent caller. The response carries details.allowed_packs, the pack ids currently buyable; retry with one of those. Never reached by a human account." },
144
+ "agent_purchase_capped": { code: 4038, status: 429, meaning: "An agent account has reached its per-credential spend cap for the current rolling window: 3 purchases, paid or declined, in the trailing 24 hours. The cap is counted per account across every credential the account has ever held, not per token, so rotating a credential does not reset it. Reached on POST /v1/billing/purchase for an agent caller, and for a human account's personal access token caller, which has its own cap of the same size counted the same way. A human purchasing through the app is never capped. The response carries details.limit, details.window_seconds and details.retry_after_seconds, and a Retry-After header naming the same wait in seconds." },
145
+ "org_last_admin": { code: 4039, status: 409, meaning: "A rights change or a member removal would leave the workspace with no member carrying is_admin. This is refused by a count check evaluated inside the update itself, not by a pre-read, so two admins concurrently dropping each other's flag cannot both succeed." },
146
+ "org_rights_invalid": { code: 4040, status: 422, meaning: "The member rights update carried no changed field, or monthly_cap_seconds fell outside the allowed range for a pooled member cap." },
147
+ "org_custody_unsupported": { code: 4043, status: 409, meaning: "The requested custody change is refused: the workspace is already firm custody, a custody switch is already scheduled, or the request asks to move to private custody, which is not reversible once transcripts carry an org_id." },
148
+ "org_reviewer_required": { code: 4044, status: 403, meaning: "The route needs the reviewer right. Carrying the admin flag alone never satisfies it, since the reviewer right is kept deliberately separate." },
149
+ "org_custodian_required": { code: 4045, status: 409, meaning: "Offboarding a member from a firm-custody workspace named no custodian to receive that member's work product." },
150
+ "org_custodian_invalid": { code: 4046, status: 422, meaning: "The named custodian is not a reviewer member of this workspace, or is the member being removed. The admin right does not qualify on its own." },
151
+ "transcript_legal_hold": { code: 4047, status: 409, meaning: "Deletion, an edit (body, translation, cleaned text, revert, retranslate), an AI-output delete or paid regeneration, a retention change or a re-file to another matter was refused because the transcript is under a legal hold, placed by a workspace admin or derived from its matter." },
152
+ "org_not_member": { code: 4048, status: 403, meaning: "The request named a workspace that the caller does not belong to." },
153
+ "org_matter_code_required": { code: 4049, status: 422, meaning: "The workspace sets matter_code_policy to required and the upload carried no matter_code. Send one, or ask a workspace admin to relax the policy." },
154
+ "org_matter_code_invalid": { code: 4050, status: 422, meaning: "matter_code or client_code is longer than 64 characters after trimming. Both are free text fields checked on length alone, and a blank or whitespace-only value is accepted and stored as no code. For a workspace (org) upload, client_code is validated here but the stored value always comes from the resolved matter's client, never from this field; a personal (no org) upload's client_code is stored exactly as sent." },
155
+ "org_usage_range_invalid": { code: 4051, status: 422, meaning: "GET /api/org/usage: from or to is missing, not a positive unix timestamp, reversed, or spans more than 366 days." },
156
+ "org_usage_group_invalid": { code: 4052, status: 422, meaning: "GET /api/org/usage: group_by is present and is not member, matter or client." },
157
+ "org_rebill_rate_invalid": { code: 4053, status: 422, meaning: "PATCH /api/org: rebill_rate_cents_per_hour is negative or above 100000. The rate is report only and never charges anyone." },
158
+ "org_asset_forbidden": { code: 4054, status: 403, meaning: "A workspace dictionary or prompt preset was created, edited or deleted by a member who holds neither the admin nor the reviewer right." },
159
+ "org_asset_limit": { code: 4055, status: 409, meaning: "The workspace already owns the maximum number of dictionaries or prompt presets. Delete one first before creating another." },
160
+ "org_letterhead_invalid": { code: 4056, status: 422, meaning: "PATCH /api/org: letterhead failed shape or length validation. Send null to clear the letterhead instead." },
161
+ "org_matter_hold_forbidden": { code: 4057, status: 403, meaning: "POST or DELETE /api/org/holds/matter from a workspace that is not in firm custody, or from a member without the reviewer right." },
162
+ "org_matter_hold_empty": { code: 4058, status: 404, meaning: "POST /api/org/holds/matter: no workspace transcript carries that matter code, so there was nothing to hold." },
163
+ "org_refund_term_renewed": { code: 4061, status: 409, meaning: "The workspace holds another paid plan term besides the invoice being refunded, so the operator's signed fee-refund command refuses and the refund is handled manually through support. No customer route returns this code; the fee refund exists only as that operator command." },
164
+ "org_self_grant_refused": { code: 4062, status: 409, meaning: "The member rights update targeted the caller's own row and asked for a right the caller does not already hold. Self-revocation, a self-targeted monthly cap change and a self-grant of the reviewer right are still allowed; a self-grant of the billing right is allowed only while no member of the workspace holds it. Otherwise another admin has to grant the right." },
165
+ "org_matter_not_found": { code: 4063, status: 404, meaning: "The matter id named in the request does not exist in this workspace." },
166
+ "org_matter_code_taken": { code: 4064, status: 409, meaning: "Another matter in this workspace already uses that matter code." },
167
+ "org_matter_closed": { code: 4065, status: 409, meaning: "That matter is closed and cannot take new work." },
168
+ "org_matter_limit": { code: 4066, status: 409, meaning: "This workspace has reached its configured matter limit." },
169
+ "org_client_not_found": { code: 4067, status: 404, meaning: "The client id named in the request does not exist in this workspace." },
170
+ "org_client_code_taken": { code: 4068, status: 409, meaning: "Another client in this workspace already uses that client code." },
171
+ "org_client_closed": { code: 4069, status: 409, meaning: "That client is closed, so its matters cannot take new work either." },
172
+ "org_client_limit": { code: 4070, status: 409, meaning: "This workspace has reached its configured client limit." },
173
+ "deletion_code_invalid": { code: 4071, status: 422, meaning: "The email_code sent to DELETE /api/me is wrong, expired, already used, or has had five wrong guesses. Request a new code with POST /api/me/deletion-code." },
174
+ "rate_limited": { code: 5e3, status: 429, meaning: "The caller has been rate limited. PAT-authenticated traffic carries a Retry-After header naming the wait in seconds; other rate-limited routes may not." },
175
+ "too_many_active_jobs": { code: 5001, status: 429, meaning: "The account already has the maximum number of concurrent pipeline jobs in flight. Wait for one to finish before starting another." },
176
+ "ai_cap_reached": { code: 5002, status: 503, meaning: "The Insights provider's own capacity is temporarily exhausted. Retry the generation after a short delay." },
177
+ "preview_daily_limit": { code: 5003, status: 429, meaning: "The caller's IP has reached the daily limit for the anonymous preview endpoint. Retry after the daily window resets, or authenticate to use the ordinary upload flow instead." },
178
+ "provider_error": { code: 6001, status: 502, meaning: "The transcription provider returned an error or could not be reached. This is usually transient; retry after a delay." },
179
+ "stripe_error": { code: 6002, status: 502, meaning: "The payment provider returned an error or could not be reached. This is usually transient; retry after a delay." },
180
+ "email_error": { code: 6003, status: 502, meaning: "The transactional email provider could not be reached or refused the message. Not directly actionable by an API caller; the underlying action may still have succeeded." },
181
+ "internal_error": { code: 9e3, status: 500, meaning: "An unexpected server-side failure occurred. Retry later; if it persists, it is a bug on the backend, not the request." },
182
+ "transcript_data_missing": { code: 9001, status: 500, meaning: "No longer emitted: no current route returns this code. It stays in the catalog, with its code, slug and status unchanged, for clients that may still key on it. It used to mean: The transcript's stored body could not be found or read where this action expected it. Treat as a transient storage fault and retry, or contact support if it persists." }
183
+ };
184
+
185
+ // src/generated/hints.ts
186
+ var HINTS = {
187
+ "unauthorized": "Your token is invalid, revoked or expired. Create a new one in the Hushscript app under Account \u2192 MCP. Changing your password revokes all tokens.",
188
+ "pat_scope_missing": 'This call needs the "{scope}" permission. Create a token that includes it. For billing, the token also needs "Let this token buy minutes" turned on when you create it, and "Let connected apps buy minutes" must be on in the app under Account \u2192 MCP.',
189
+ "insufficient_balance": "Not enough minutes: {needed} needed, {balance} available. Buy a pack with the SDK's buy method or in the app.",
190
+ "rate_limited": "Too many requests. The SDK already waited and retried; slow down or try again in {retryAfter} s.",
191
+ "too_many_active_jobs": "50 jobs are already in progress. Wait for some to finish.",
192
+ "agent_purchase_capped": "This account has used its 3 purchase attempts for the last 24 hours. Try again in {retryAfter} s.",
193
+ "agent_pack_locked": "New agent accounts can buy {allowedPacks} until earlier purchases settle.",
194
+ "pat_purchase_requires_app": "The saved card was declined or can't be charged automatically. Buy this pack in the Hushscript app.",
195
+ "pat_purchase_requires_authentication": "Your bank wants to confirm this payment. Buy this pack in the Hushscript app.",
196
+ "no_default_card": "No saved card. Open the add-card link from the SDK's billing methods in a browser, or add a card in the app.",
197
+ "unsupported_media": "Supported: MP3, M4A/MP4/MOV, WAV, FLAC, OGG/Opus. Convert it first, e.g. ffmpeg -i input -vn -c:a libopus output.opus",
198
+ "duration_unreadable": "Could not read the file's duration. Pass durationSeconds.",
199
+ "duration_out_of_range": "Files must be between 15 seconds and 10 hours long.",
200
+ "size_out_of_range": "Files must be at most 5 GB.",
201
+ "daily_limit": "This upload would pass your daily transcription limit ({used} of {max} min used in the last 24 h).",
202
+ "policy_version_stale": "Accept the current terms version: {currentVersion}.",
203
+ "wrong_state": "The job has already moved on. Fetch the job to see where it is.",
204
+ "resume_file_mismatch": "This is not the file the upload started with. Pass the original file, or start a new transcription.",
205
+ "resume_file_required": "The upload was not finished. Pass the original file to resume it.",
206
+ "resume_job_missing": "This job no longer exists. Start a new transcription.",
207
+ "stream_size_mismatch": "The stream did not match the size you passed. Pass the exact size in bytes.",
208
+ "default_card_required": "You have more than one saved card and none is the default. Set a default card with the SDK's billing methods or in the app."
209
+ };
210
+
211
+ // src/generated/scopes.ts
212
+ var OPERATION_SCOPES = {
213
+ "DELETE /v1/org/transcripts/{id}/hold": "org:write",
214
+ "GET /v1/account": "account:read",
215
+ "GET /v1/billing/cards": "billing",
216
+ "GET /v1/billing/holds": "account:read",
217
+ "GET /v1/billing/ledger": "account:read",
218
+ "GET /v1/billing/packs": "billing",
219
+ "GET /v1/billing/transactions": "account:read",
220
+ "GET /v1/jobs": "transcribe",
221
+ "GET /v1/jobs/{id}": "transcribe",
222
+ "GET /v1/org": "org:read",
223
+ "GET /v1/org/audit": "org:read",
224
+ "GET /v1/org/audit.csv": "org:read",
225
+ "GET /v1/org/invoices": "org:read",
226
+ "GET /v1/org/ledger": "org:read",
227
+ "GET /v1/org/ledger.csv": "org:read",
228
+ "GET /v1/org/matters": "org:read",
229
+ "GET /v1/org/members": "org:read",
230
+ "GET /v1/org/transcripts": "org:read",
231
+ "GET /v1/org/usage": "org:read",
232
+ "GET /v1/org/usage.csv": "org:read",
233
+ "GET /v1/transcription-context": "context",
234
+ "GET /v1/transcripts": "transcripts:read",
235
+ "GET /v1/transcripts/deleted": "transcripts:read",
236
+ "GET /v1/transcripts/{id}": "transcripts:read",
237
+ "GET /v1/uploads/{job}/part-urls": "transcribe",
238
+ "PATCH /v1/org/members/{user_id}": "org:write",
239
+ "POST /v1/agent/credentials/rotate": "pat:rotate",
240
+ "POST /v1/billing/cards/default": "billing",
241
+ "POST /v1/billing/purchase": "billing",
242
+ "POST /v1/billing/setup-session": "billing",
243
+ "POST /v1/org/custody": "org:write",
244
+ "POST /v1/org/transcripts/{id}/hold": "org:write",
245
+ "POST /v1/uploads": "transcribe",
246
+ "POST /v1/uploads/{job}/complete": "transcribe",
247
+ "POST /v1/uploads/{job}/heartbeat": "transcribe",
248
+ "PUT /v1/uploads/{job}/parts/{n}": "transcribe"
249
+ };
250
+
251
+ // src/http/retry.ts
252
+ var MAX_RETRY_AFTER_SECONDS = 60;
253
+ var BACKOFF_CAP_MS = 8e3;
254
+ var BACKOFF_BASE_MS = 500;
255
+ var PURCHASE_MAX_RETRIES = 1;
256
+ var CREATE_RATE_LIMIT_SCHEDULE_MS = [15e3, 3e4, 6e4];
257
+ var CREATE_ACTIVE_JOBS_WAIT_MS = 3e4;
258
+ var CREATE_ACTIVE_JOBS_RETRIES = 9;
259
+ var EXPLICIT_CLASSES = {
260
+ "POST /v1/uploads": "upload-create",
261
+ "PUT /v1/uploads/{job}/parts/{n}": "idempotent",
262
+ "POST /v1/uploads/{job}/heartbeat": "safe",
263
+ "POST /v1/uploads/{job}/complete": "idempotent",
264
+ "POST /v1/billing/purchase": "purchase",
265
+ "POST /v1/billing/setup-session": "purchase",
266
+ "POST /v1/agent/accounts/{signup_id}/claim": "never",
267
+ "POST /v1/agent/credentials/rotate": "never",
268
+ "POST /v1/agent/accounts": "never"
269
+ };
270
+ function retryClassFor(method, template) {
271
+ const verb = method.toUpperCase();
272
+ if (verb === "GET" || verb === "HEAD") return "safe";
273
+ return EXPLICIT_CLASSES[`${verb} ${template}`] ?? "mutate";
274
+ }
275
+ function backoffMs(attempt2, random) {
276
+ return random() * Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** attempt2);
277
+ }
278
+ var MAX_RETRY_AFTER_REPORTED = Number.MAX_SAFE_INTEGER;
279
+ function parseRetryAfter(header) {
280
+ if (header === null) return null;
281
+ const text = header.trim();
282
+ if (!/^\d+$/.test(text)) return null;
283
+ return Math.min(Number(text), MAX_RETRY_AFTER_REPORTED);
284
+ }
285
+ var STOP = { retry: false };
286
+ function retriableFailure(retryClass, status, sent) {
287
+ switch (retryClass) {
288
+ case "safe":
289
+ return status === null || status === 429 || status === 500 || status >= 502 && status <= 504;
290
+ case "idempotent":
291
+ case "upload-create":
292
+ return status === null || status === 429 || status >= 500;
293
+ case "purchase":
294
+ return status === null || status === 502 || status === 503;
295
+ case "mutate":
296
+ return status === null && !sent;
297
+ // Stryker disable next-line ConditionalExpression,StringLiteral: without this case the switch returns undefined, which reads as false too
298
+ case "never":
299
+ return false;
300
+ }
301
+ }
302
+ function planRetry(input) {
303
+ const { retryClass, status, retryAfter, retriesDone, maxRetries } = input;
304
+ if (!retriableFailure(retryClass, status, input.sent)) return STOP;
305
+ if ((retryAfter ?? 0) > MAX_RETRY_AFTER_SECONDS) return STOP;
306
+ let delayMs = backoffMs(retriesDone, input.random);
307
+ let limit = retryClass === "purchase" ? Math.min(maxRetries, PURCHASE_MAX_RETRIES) : maxRetries;
308
+ if (retryClass === "upload-create" && status === 429) {
309
+ if (input.slug === "too_many_active_jobs") {
310
+ if (maxRetries === 0 || input.activeJobsDone >= CREATE_ACTIVE_JOBS_RETRIES) return STOP;
311
+ limit = Infinity;
312
+ delayMs = CREATE_ACTIVE_JOBS_WAIT_MS;
313
+ } else if (retryAfter === null) {
314
+ if (input.rateLimitsDone >= CREATE_RATE_LIMIT_SCHEDULE_MS.length) return STOP;
315
+ delayMs = CREATE_RATE_LIMIT_SCHEDULE_MS[input.rateLimitsDone] ?? delayMs;
316
+ }
317
+ }
318
+ if (retriesDone >= limit) return STOP;
319
+ return { retry: true, delayMs: retryAfter === null ? delayMs : retryAfter * 1e3 };
320
+ }
321
+
322
+ // src/http/operations.ts
323
+ function compile(key) {
324
+ const space = key.indexOf(" ");
325
+ const literals = key.slice(space + 1).split(/\{[^{}]+\}/);
326
+ const pattern = literals.map((literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)).join("[^/]+");
327
+ return {
328
+ key,
329
+ method: key.slice(0, space),
330
+ pattern: new RegExp(`^${pattern}$`),
331
+ pieces: literals.length
332
+ };
333
+ }
334
+ function templateMatcher(keys) {
335
+ const templates = [...new Set(keys)].map(compile).sort((a, b) => a.pieces - b.pieces);
336
+ return (verb, pathname) => templates.find((t) => t.method === verb && t.pattern.test(pathname))?.key;
337
+ }
338
+ var match = templateMatcher([...Object.keys(OPERATION_SCOPES), ...Object.keys(EXPLICIT_CLASSES)]);
339
+ function operationFor(method, pathname) {
340
+ const verb = method.toUpperCase();
341
+ return match(verb, pathname) ?? `${verb} ${pathname}`;
342
+ }
343
+ function resolveOperation(operation) {
344
+ const space = operation.indexOf(" ");
345
+ return operationFor(operation.slice(0, space), operation.slice(space + 1));
346
+ }
347
+ function hasIdempotencyKey(body) {
348
+ if (body === null) return false;
349
+ try {
350
+ const key = JSON.parse(body)?.idempotency_key;
351
+ return typeof key === "string" && key !== "";
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+ function retryClassForRequest(method, pathname, body = null) {
357
+ const operation = operationFor(method, pathname);
358
+ const retryClass = retryClassFor(method, operation.slice(operation.indexOf(" ") + 1));
359
+ return retryClass === "upload-create" && !hasIdempotencyKey(body) ? "mutate" : retryClass;
360
+ }
361
+ var REDACTED = "hsr\u2026[redacted]";
362
+ var TOKEN_SHAPE = /hsr\d+:[a-z]+:pat\.[0-9a-f]+\.[0-9a-f]+/g;
363
+ var MIN_SECRET_LENGTH = 8;
364
+ var MIN_SECRET_TAIL = 16;
365
+ var URL_QUERY = /\b([a-z][a-z\d+.-]*:\/\/[^\s?#"'<>]+)\?[^\s"'<>#]*[^\s"'<>#).,;:]/gi;
366
+ var QUERY_REDACTED = "?[redacted]";
367
+ var secrets = /* @__PURE__ */ new Set();
368
+ function registerSecret(secret) {
369
+ if (secret.length < MIN_SECRET_LENGTH) return;
370
+ remember(secret);
371
+ const marker = secret.indexOf("pat.");
372
+ if (marker === -1) return;
373
+ const tail = secret.slice(marker + "pat.".length);
374
+ for (const part of [tail, tail.slice(tail.lastIndexOf(".") + 1)]) {
375
+ if (part.length >= MIN_SECRET_TAIL) remember(part);
376
+ }
377
+ }
378
+ function remember(secret) {
379
+ secrets.add(secret);
380
+ secrets.add(encodeURIComponent(secret));
381
+ }
382
+ function redact(text) {
383
+ let out = text;
384
+ for (const secret of secrets) out = out.replaceAll(secret, REDACTED);
385
+ return out.replace(TOKEN_SHAPE, REDACTED).replace(URL_QUERY, `$1${QUERY_REDACTED}`);
386
+ }
387
+ function redactValue(value) {
388
+ if (typeof value === "string") return redact(value);
389
+ if (Array.isArray(value)) return value.map(redactValue);
390
+ if (typeof value === "object" && value !== null) {
391
+ return Object.fromEntries(
392
+ Object.entries(value).map(([key, item]) => [redact(key), redactValue(item)])
393
+ );
394
+ }
395
+ return value;
396
+ }
397
+ function redactCause(cause) {
398
+ if (!(cause instanceof Error)) return redactValue(cause);
399
+ const copy = new Error(redact(cause.message));
400
+ copy.name = cause.name;
401
+ if (cause.stack !== void 0) copy.stack = redact(cause.stack);
402
+ const code = cause.code;
403
+ if (typeof code === "string") Object.assign(copy, { code });
404
+ if (cause.cause !== void 0) copy.cause = redactCause(cause.cause);
405
+ return copy;
406
+ }
407
+ function withRedactedField(name, value, field) {
408
+ const printable = () => ({ ...value, [field]: REDACTED });
409
+ Object.defineProperty(value, inspect.custom, { value: printable });
410
+ Object.defineProperty(value, "toString", {
411
+ value: () => `${name} ${JSON.stringify(printable())}`
412
+ });
413
+ return value;
414
+ }
415
+
416
+ // src/errors.ts
417
+ var PLACEHOLDER = /\{(\w+)\}/g;
418
+ function fillHint(template, values) {
419
+ const known = (name) => values[name] !== void 0 && values[name] !== null;
420
+ const kept = template.split(/(?<=\.) /).filter((sentence) => [...sentence.matchAll(PLACEHOLDER)].every((m) => known(m[1] ?? "")));
421
+ if (kept.length === 0) return null;
422
+ return kept.join(" ").replace(PLACEHOLDER, (_whole, name) => String(values[name]));
423
+ }
424
+ function minutes(seconds2, round) {
425
+ return seconds2 === void 0 ? void 0 : round(seconds2 / 60);
426
+ }
427
+ function hintValuesFrom(inputs) {
428
+ return {
429
+ scope: inputs.scope,
430
+ needed: minutes(inputs.neededSeconds, Math.ceil),
431
+ balance: minutes(inputs.balanceSeconds, Math.trunc),
432
+ used: minutes(inputs.usedSeconds, Math.trunc),
433
+ max: minutes(inputs.maxSeconds, Math.trunc),
434
+ retryAfter: inputs.retryAfter,
435
+ allowedPacks: inputs.allowedPacks?.join(", "),
436
+ currentVersion: inputs.currentVersion
437
+ };
438
+ }
439
+ function renderHint(slug, values) {
440
+ const template = HINTS[slug];
441
+ return template === void 0 ? null : fillHint(template, values);
442
+ }
443
+ function resolveHint(init) {
444
+ if (init.hint !== void 0) return init.hint === null ? null : redact(init.hint);
445
+ const hint = renderHint(init.code, init.hintValues ?? {});
446
+ return hint === null ? null : redact(hint);
447
+ }
448
+ var HushscriptError = class extends Error {
449
+ code;
450
+ numericCode;
451
+ status;
452
+ hint;
453
+ details;
454
+ retryAfter;
455
+ /** Set on an error `transcribe()` raised after its create was sent, or `buy()` after its purchase may have been sent (a failure provably before any attempt went out carries none): passing it back replays the same job or purchase instead of making a second one. */
456
+ idempotencyKey = null;
457
+ /** Set once the job id is known; `resume()` takes it. */
458
+ resumeToken = null;
459
+ constructor(init) {
460
+ super(
461
+ redact(init.message),
462
+ init.cause === void 0 ? void 0 : { cause: redactCause(init.cause) }
463
+ );
464
+ this.name = new.target.name;
465
+ this.code = redact(init.code);
466
+ this.numericCode = init.numericCode ?? null;
467
+ this.status = init.status ?? null;
468
+ this.hint = resolveHint(init);
469
+ this.details = redactValue(init.details ?? {});
470
+ this.retryAfter = init.retryAfter ?? null;
471
+ }
472
+ };
473
+ var failedBeforeSending = /* @__PURE__ */ new WeakSet();
474
+ function markNotSent(error) {
475
+ failedBeforeSending.add(error);
476
+ return error;
477
+ }
478
+ function wasNotSent(error) {
479
+ return failedBeforeSending.has(error);
480
+ }
481
+ function describeError(error) {
482
+ if (error instanceof Error) {
483
+ return error.message === "" ? error.name : `${error.name}: ${error.message}`;
484
+ }
485
+ return typeof error === "string" ? `thrown string: ${error}` : `thrown ${typeof error}`;
486
+ }
487
+ function asHushscriptError(error, sent) {
488
+ if (error instanceof HushscriptError) return error;
489
+ const when = sent === void 0 ? "" : sent ? " after the request was sent" : " before the request was sent";
490
+ return new HushscriptError({
491
+ code: "unexpected_error",
492
+ message: `The SDK failed unexpectedly${when} (${describeError(error)}).`,
493
+ cause: error
494
+ });
495
+ }
496
+ var MISSING_TOKEN_HINT = "Set HUSHSCRIPT_TOKEN or pass a token. Create one in the Hushscript app: Account \u2192 MCP (app.hushscript.com/account/mcp).";
497
+ var ConfigurationError = class _ConfigurationError extends HushscriptError {
498
+ constructor(init) {
499
+ super({ ...init, code: init.code ?? "config_invalid" });
500
+ }
501
+ static missingToken() {
502
+ return new _ConfigurationError({
503
+ message: "No Hushscript token: set HUSHSCRIPT_TOKEN or pass a token.",
504
+ hint: MISSING_TOKEN_HINT
505
+ });
506
+ }
507
+ };
508
+ var PreflightError = class extends HushscriptError {
509
+ };
510
+ var UnsupportedMediaError = class extends PreflightError {
511
+ };
512
+ var FileLimitError = class extends PreflightError {
513
+ };
514
+ var DailyLimitError = class extends PreflightError {
515
+ };
516
+ var ResumeMismatchError = class extends PreflightError {
517
+ };
518
+ var ApiError = class extends HushscriptError {
519
+ };
520
+ var AuthenticationError = class extends ApiError {
521
+ };
522
+ var PermissionDeniedError = class extends ApiError {
523
+ };
524
+ var InsufficientBalanceError = class extends ApiError {
525
+ };
526
+ var NotFoundError = class extends ApiError {
527
+ };
528
+ var ConflictError = class extends ApiError {
529
+ };
530
+ var ValidationError = class extends ApiError {
531
+ };
532
+ var RateLimitError = class extends ApiError {
533
+ };
534
+ var PaymentMethodRequiredError = class extends HushscriptError {
535
+ constructor(init) {
536
+ super({ ...init, code: init.code ?? "no_default_card" });
537
+ }
538
+ };
539
+ var UploadError = class extends HushscriptError {
540
+ };
541
+ var JobFailedError = class extends HushscriptError {
542
+ error;
543
+ failureKind;
544
+ jobId;
545
+ constructor(init) {
546
+ super({ ...init, code: init.code ?? "job_failed" });
547
+ this.error = init.error === null ? null : redact(init.error);
548
+ this.failureKind = init.failureKind;
549
+ this.jobId = init.jobId;
550
+ }
551
+ };
552
+ var WaitTimeoutError = class extends HushscriptError {
553
+ /** `resumeToken` is null only for `jobs.wait`, which has no idempotency key to build a token from. */
554
+ constructor(init) {
555
+ super({ ...init, code: init.code ?? "wait_timeout" });
556
+ this.resumeToken = init.resumeToken;
557
+ }
558
+ };
559
+ var AbortError = class extends HushscriptError {
560
+ constructor(init) {
561
+ super({ ...init, code: init.code ?? "aborted" });
562
+ this.resumeToken = init.resumeToken ?? null;
563
+ }
564
+ };
565
+ var NetworkError = class extends HushscriptError {
566
+ constructor(init) {
567
+ super({ ...init, code: init.code ?? "network_error" });
568
+ }
569
+ };
570
+ var OUTCOME_HINTS = {
571
+ "POST /v1/agent/accounts/{signup_id}/claim": "The claim may have succeeded. Retrying will fail with agent_claim_invalid. If you have no token, contact support with the signup id.",
572
+ "POST /v1/agent/credentials/rotate": "The old token may already be revoked. Try a request with it; if it fails, the new token was lost and the account needs support."
573
+ };
574
+ var OutcomeUnknownError = class extends HushscriptError {
575
+ #receivedToken;
576
+ constructor(init) {
577
+ super({ ...init, code: init.code ?? "outcome_unknown" });
578
+ this.#receivedToken = init.receivedToken ?? null;
579
+ }
580
+ /**
581
+ * The string a rotate answer carried as the new token when it was not a usable one, so a caller
582
+ * can still look at (or rescue) it. A getter over a private field: it is left out of `inspect`,
583
+ * `JSON.stringify` and every message. Null for every other outcome.
584
+ */
585
+ get receivedToken() {
586
+ return this.#receivedToken;
587
+ }
588
+ static hintFor(operation) {
589
+ return OUTCOME_HINTS[operation] ?? null;
590
+ }
591
+ };
592
+ var RATE_LIMIT_SLUGS = /* @__PURE__ */ new Set([
593
+ "rate_limited",
594
+ "too_many_active_jobs",
595
+ "agent_purchase_capped",
596
+ "url_target_busy"
597
+ ]);
598
+ var BALANCE_SLUGS = /* @__PURE__ */ new Set([
599
+ "insufficient_balance",
600
+ "org_insufficient_balance",
601
+ "org_pool_expired",
602
+ "org_plan_frozen"
603
+ ]);
604
+ var UPLOAD_SLUGS = /* @__PURE__ */ new Set(["upload_incomplete", "size_exceeded"]);
605
+ var JOB_OPERATION = /^\w+ \/v1\/(?:jobs|uploads)(?:\/|$)/;
606
+ function classFor(status, slug) {
607
+ if (UPLOAD_SLUGS.has(slug)) return UploadError;
608
+ if (RATE_LIMIT_SLUGS.has(slug)) return RateLimitError;
609
+ if (BALANCE_SLUGS.has(slug)) return InsufficientBalanceError;
610
+ switch (status) {
611
+ case 400:
612
+ case 422:
613
+ return ValidationError;
614
+ case 401:
615
+ return AuthenticationError;
616
+ case 402:
617
+ return InsufficientBalanceError;
618
+ case 403:
619
+ return PermissionDeniedError;
620
+ case 404:
621
+ return NotFoundError;
622
+ case 409:
623
+ return ConflictError;
624
+ case 429:
625
+ return RateLimitError;
626
+ default:
627
+ return ApiError;
628
+ }
629
+ }
630
+ function isRecord(value) {
631
+ return typeof value === "object" && value !== null && !Array.isArray(value);
632
+ }
633
+ function numberOf(value) {
634
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
635
+ }
636
+ function hintValuesFor(details, retryAfter, operation) {
637
+ const nested = isRecord(details["details"]) ? details["details"] : {};
638
+ const packs = nested["allowedPacks"];
639
+ const version = details["currentVersion"] ?? nested["currentVersion"];
640
+ return hintValuesFrom({
641
+ scope: OPERATION_SCOPES[resolveOperation(operation)],
642
+ neededSeconds: numberOf(details["neededSeconds"]),
643
+ balanceSeconds: numberOf(details["balanceSeconds"]),
644
+ retryAfter,
645
+ allowedPacks: Array.isArray(packs) ? packs : void 0,
646
+ currentVersion: typeof version === "string" ? version : numberOf(version)
647
+ });
648
+ }
649
+ function errorFromResponse(res, body, operation) {
650
+ const record = isRecord(body) ? body : {};
651
+ const slug = typeof record["error"] === "string" ? record["error"] : null;
652
+ const numericCode = Number.isSafeInteger(record["code"]) ? record["code"] : null;
653
+ const code = slug ?? `http_${String(res.status)}`;
654
+ const extras = Object.entries(record).filter(([key]) => key !== "error" && key !== "code");
655
+ const details = camelize(Object.fromEntries(extras));
656
+ const nested = isRecord(details["details"]) ? details["details"] : {};
657
+ const bodyRetry = nested["retryAfterSeconds"];
658
+ const retryAfter = parseRetryAfter(res.headers.get("retry-after")) ?? (typeof bodyRetry === "number" && Number.isFinite(bodyRetry) && bodyRetry >= 0 ? Math.min(bodyRetry, MAX_RETRY_AFTER_REPORTED) : null);
659
+ const meaning = slug === null ? void 0 : ERROR_CATALOG[slug]?.meaning;
660
+ const message = meaning ?? (slug === null ? `The API answered ${String(res.status)}.` : `${slug} (HTTP ${String(res.status)})`);
661
+ const ErrorType = classFor(res.status, code);
662
+ return new ErrorType({
663
+ code,
664
+ message,
665
+ numericCode,
666
+ status: res.status,
667
+ details,
668
+ retryAfter,
669
+ // `wrong_state` names a job call, so only job and upload operations carry that hint.
670
+ ...code === "wrong_state" && !JOB_OPERATION.test(resolveOperation(operation)) ? { hint: null } : {},
671
+ hintValues: hintValuesFor(details, retryAfter, operation)
672
+ });
673
+ }
674
+
675
+ // src/http/pacing.ts
676
+ var TokenBucket = class {
677
+ #capacity;
678
+ #windowMs;
679
+ #clock;
680
+ #stamps = [];
681
+ constructor(capacity, windowMs, clock) {
682
+ this.#capacity = capacity;
683
+ this.#windowMs = windowMs;
684
+ this.#clock = clock;
685
+ }
686
+ /** Resolves with the milliseconds spent waiting (0 when a slot was free). */
687
+ async acquire(signal) {
688
+ let waited = 0;
689
+ for (; ; ) {
690
+ const now = this.#clock.now();
691
+ while (now - (this.#stamps[0] ?? Infinity) >= this.#windowMs) {
692
+ this.#stamps.shift();
693
+ }
694
+ const oldest = this.#stamps.length < this.#capacity ? void 0 : this.#stamps[0];
695
+ if (oldest === void 0) {
696
+ this.#stamps.push(now);
697
+ return waited;
698
+ }
699
+ const wait = oldest + this.#windowMs - now;
700
+ await this.#clock.sleep(wait, signal);
701
+ waited += wait;
702
+ }
703
+ }
704
+ };
705
+
706
+ // src/log.ts
707
+ function formatAttempt(entry) {
708
+ const parts = [
709
+ "hushscript",
710
+ entry.method,
711
+ entry.target,
712
+ `status=${String(entry.status)}`,
713
+ `attempt=${String(entry.attempt)}`,
714
+ `duration_ms=${String(entry.durationMs)}`
715
+ ];
716
+ if (entry.retryDelayMs !== null) parts.push(`retry_in_ms=${String(entry.retryDelayMs)}`);
717
+ return parts.join(" ");
718
+ }
719
+ function stderrSink(line) {
720
+ console.error(line);
721
+ }
722
+ function createLogger(level, sink = stderrSink) {
723
+ return {
724
+ enabled: level === "debug",
725
+ attempt(entry) {
726
+ if (level === "debug") sink(redact(formatAttempt(entry)));
727
+ }
728
+ };
729
+ }
730
+ function describeTarget(url2, apiOrigin) {
731
+ if (url2.origin === apiOrigin && !url2.pathname.startsWith("/__r2/")) return url2.pathname;
732
+ const part = url2.searchParams.get("partNumber") ?? "";
733
+ return /^\d+$/.test(part) ? `r2:part/${part}` : "r2:part";
734
+ }
735
+
736
+ // src/config.ts
737
+ var DEFAULT_BASE_URL = "https://api.hushscript.com";
738
+ var DEFAULT_TIMEOUT_SECONDS = 60;
739
+ var MAX_TIMEOUT_SECONDS = 3600;
740
+ var DEFAULT_MAX_RETRIES = 3;
741
+ var PART_TIMEOUT_MS = 3e5;
742
+ var CREATES_PER_WINDOW = 4;
743
+ var CREATE_WINDOW_MS = 6e4;
744
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
745
+ var ResolvedConfig = class {
746
+ baseUrl;
747
+ timeoutMs;
748
+ /** Per upload part PUT; not configurable. */
749
+ partTimeoutMs;
750
+ maxRetries;
751
+ logLevel;
752
+ clock;
753
+ logger;
754
+ random;
755
+ /** Paces `POST /v1/uploads` for this client instance. */
756
+ createPacer;
757
+ userAgent;
758
+ #token;
759
+ constructor(token, values) {
760
+ this.#token = token;
761
+ Object.defineProperty(this, inspect.custom, { value: () => this.toString() });
762
+ this.baseUrl = values.baseUrl;
763
+ this.timeoutMs = values.timeoutMs;
764
+ this.partTimeoutMs = values.partTimeoutMs;
765
+ this.maxRetries = values.maxRetries;
766
+ this.logLevel = values.logLevel;
767
+ this.clock = values.clock;
768
+ this.logger = values.logger;
769
+ this.random = values.random;
770
+ this.createPacer = values.createPacer;
771
+ this.userAgent = values.userAgent;
772
+ }
773
+ authorization() {
774
+ if (this.#token === null) throw ConfigurationError.missingToken();
775
+ return `Bearer ${this.#token}`;
776
+ }
777
+ /** Replaces the token for every later request (token rotation); the new one is redacted from then on. */
778
+ setToken(token) {
779
+ registerSecret(token);
780
+ this.#token = checkToken(token);
781
+ }
782
+ toJSON() {
783
+ return {
784
+ token: REDACTED,
785
+ baseUrl: this.baseUrl,
786
+ timeoutMs: this.timeoutMs,
787
+ maxRetries: this.maxRetries,
788
+ logLevel: this.logLevel
789
+ };
790
+ }
791
+ toString() {
792
+ return `ResolvedConfig ${JSON.stringify(this.toJSON())}`;
793
+ }
794
+ };
795
+ function runtime() {
796
+ const deno = globalThis.Deno;
797
+ if (deno?.version?.deno !== void 0) return `deno/${deno.version.deno}`;
798
+ const bun = process.versions["bun"];
799
+ if (bun !== void 0) return `bun/${bun}`;
800
+ return `node/${process.versions.node}`;
801
+ }
802
+ function setValue(value) {
803
+ if (value === null || value === void 0) return void 0;
804
+ if (typeof value !== "string") return value;
805
+ return value.trim() === "" ? void 0 : value;
806
+ }
807
+ function checkBaseUrl(raw) {
808
+ let url2;
809
+ try {
810
+ url2 = new URL(raw);
811
+ } catch {
812
+ throw new ConfigurationError({ message: "baseUrl is not a valid URL." });
813
+ }
814
+ const loopback = url2.protocol === "http:" && LOOPBACK_HOSTS.has(url2.hostname);
815
+ if (url2.protocol !== "https:" && !loopback) {
816
+ throw new ConfigurationError({
817
+ message: "baseUrl must be https://, or http:// to localhost, 127.0.0.1 or [::1]."
818
+ });
819
+ }
820
+ if (url2.pathname !== "/" || url2.search !== "" || url2.hash !== "" || /[?#]/.test(raw)) {
821
+ throw new ConfigurationError({ message: "baseUrl must not have a path, query or fragment." });
822
+ }
823
+ return raw.replace(/\/+$/, "");
824
+ }
825
+ function isBrowser() {
826
+ return globalThis.window !== void 0 && globalThis.document !== void 0;
827
+ }
828
+ function checkLogLevel(raw) {
829
+ if (raw === "off" || raw === "debug") return raw;
830
+ throw new ConfigurationError({ message: 'The log level must be "off" or "debug".' });
831
+ }
832
+ function refuseBrowser(allowed) {
833
+ if (isBrowser() && allowed !== true) {
834
+ throw new ConfigurationError({
835
+ message: "The Hushscript SDK refuses to run in a browser: a token in a browser bundle is a leaked token. Call it from a server."
836
+ });
837
+ }
838
+ }
839
+ function checkToken(raw) {
840
+ if (typeof raw !== "string") {
841
+ throw new ConfigurationError({ message: "token must be a string." });
842
+ }
843
+ const token = raw.trim();
844
+ if (token === "") throw ConfigurationError.missingToken();
845
+ if (!token.startsWith("hsr")) {
846
+ throw new ConfigurationError({ message: "This doesn't look like a Hushscript token." });
847
+ }
848
+ if (!/^[\u0021-\u007e]+$/u.test(token)) {
849
+ throw new ConfigurationError({
850
+ message: "The token has spaces, control characters or non-ASCII characters in it; check how it was copied."
851
+ });
852
+ }
853
+ return token;
854
+ }
855
+ function build(token, options, env, internals) {
856
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_SECONDS;
857
+ if (!Number.isFinite(timeout) || timeout <= 0) {
858
+ throw new ConfigurationError({ message: "timeout must be a positive number of seconds." });
859
+ }
860
+ if (timeout > MAX_TIMEOUT_SECONDS) {
861
+ throw new ConfigurationError({
862
+ message: `timeout must be at most ${String(MAX_TIMEOUT_SECONDS)} seconds.`
863
+ });
864
+ }
865
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
866
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
867
+ throw new ConfigurationError({ message: "maxRetries must be a whole number, 0 or more." });
868
+ }
869
+ const clock = options.clock ?? systemClock;
870
+ const logLevel = checkLogLevel(
871
+ setValue(options.logLevel) ?? setValue(env["HUSHSCRIPT_LOG"]) ?? "off"
872
+ );
873
+ if (token !== null) registerSecret(token);
874
+ return new ResolvedConfig(token, {
875
+ baseUrl: checkBaseUrl(
876
+ setValue(options.baseUrl) ?? setValue(env["HUSHSCRIPT_BASE_URL"]) ?? DEFAULT_BASE_URL
877
+ ),
878
+ timeoutMs: timeout * 1e3,
879
+ partTimeoutMs: PART_TIMEOUT_MS,
880
+ maxRetries,
881
+ logLevel,
882
+ clock,
883
+ logger: createLogger(logLevel, internals.logSink),
884
+ random: internals.random ?? Math.random,
885
+ createPacer: new TokenBucket(CREATES_PER_WINDOW, CREATE_WINDOW_MS, clock),
886
+ userAgent: `hushscript-typescript/${package_default.version} ${runtime()}`
887
+ });
888
+ }
889
+ function resolveConfig(options = {}, internals = {}) {
890
+ const env = internals.env ?? process.env;
891
+ refuseBrowser(options.dangerouslyAllowBrowser);
892
+ const token = checkToken(setValue(options.token) ?? setValue(env["HUSHSCRIPT_TOKEN"]) ?? "");
893
+ return build(token, options, env, internals);
894
+ }
895
+ function resolveTokenlessConfig(options = {}, internals = {}) {
896
+ refuseBrowser(void 0);
897
+ return build(null, options, internals.env ?? process.env, internals);
898
+ }
899
+
900
+ // src/depth.ts
901
+ var MAX_BODY_DEPTH = 64;
902
+ function nestsDeeperThan(root, limit) {
903
+ const pending = [{ value: root, depth: 0 }];
904
+ for (let next = pending.pop(); next !== void 0; next = pending.pop()) {
905
+ const { value, depth } = next;
906
+ if (typeof value !== "object" || value === null) continue;
907
+ if (depth >= limit) return true;
908
+ for (const child of Object.values(value)) pending.push({ value: child, depth: depth + 1 });
909
+ }
910
+ return false;
911
+ }
912
+
913
+ // src/shape.ts
914
+ var MAX_UNIX_SECONDS = 864e10;
915
+ function problem(expected) {
916
+ return { path: [], expected };
917
+ }
918
+ function describeProblem({ path, expected }) {
919
+ const where = path.reduce(
920
+ (out, part) => typeof part === "number" ? `${out}[${String(part)}]` : out === "" ? part : `${out}.${part}`,
921
+ ""
922
+ );
923
+ return where === "" ? `expected ${expected}` : `${where}: expected ${expected}`;
924
+ }
925
+ var str = (value) => typeof value === "string" ? null : problem("a string");
926
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? null : problem("a number");
927
+ var positiveInt = (value) => Number.isSafeInteger(value) && value > 0 ? null : problem("a positive whole number");
928
+ function positiveIntAtMost(max) {
929
+ return (value) => Number.isSafeInteger(value) && value > 0 && value <= max ? null : problem(`a positive whole number up to ${String(max)}`);
930
+ }
931
+ function isPathSegment(value) {
932
+ return typeof value === "string" && value !== "" && value !== "." && value !== ".." && !/[\uD800-\uDFFF]/u.test(value);
933
+ }
934
+ var pathId = (value) => isPathSegment(value) ? null : problem("a usable id");
935
+ var bool = (value) => typeof value === "boolean" ? null : problem("a boolean");
936
+ var anything = () => null;
937
+ var timestamp = (value) => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= MAX_UNIX_SECONDS ? null : problem("a unix timestamp in seconds");
938
+ var url = (value) => typeof value === "string" && URL.canParse(value) ? null : problem("a URL");
939
+ function nullable(shape) {
940
+ return (value) => value === null ? null : shape(value);
941
+ }
942
+ function optional(shape) {
943
+ return (value) => value === void 0 ? null : shape(value);
944
+ }
945
+ function arrayOf(shape) {
946
+ return (value) => {
947
+ if (!Array.isArray(value)) return problem("an array");
948
+ for (let index = 0; index < value.length; index += 1) {
949
+ const found = shape(value[index]);
950
+ if (found !== null) return { path: [index, ...found.path], expected: found.expected };
951
+ }
952
+ return null;
953
+ };
954
+ }
955
+ function isRecord2(value) {
956
+ return typeof value === "object" && value !== null && !Array.isArray(value);
957
+ }
958
+ function recordOf(shape) {
959
+ return (value) => {
960
+ if (!isRecord2(value)) return problem("an object");
961
+ for (const [key, item] of Object.entries(value)) {
962
+ const found = shape(item);
963
+ if (found !== null) return { path: [key, ...found.path], expected: found.expected };
964
+ }
965
+ return null;
966
+ };
967
+ }
968
+ function objectOf(fields) {
969
+ const entries = Object.entries(fields);
970
+ return (value) => {
971
+ if (!isRecord2(value)) return problem("an object");
972
+ for (const [key, shape] of entries) {
973
+ const found = shape(Object.hasOwn(value, key) ? value[key] : void 0);
974
+ if (found !== null) return { path: [key, ...found.path], expected: found.expected };
975
+ }
976
+ return null;
977
+ };
978
+ }
979
+
980
+ // src/transcribe/common.ts
981
+ function aborted(signal) {
982
+ return new AbortError({ message: "The request was aborted.", cause: signal.reason });
983
+ }
984
+ async function sleepOrAbort(clock, ms, signal) {
985
+ try {
986
+ await clock.sleep(ms, signal);
987
+ } catch (error) {
988
+ if (signal.aborted) throw aborted(signal);
989
+ throw error;
990
+ }
991
+ }
992
+ function badResponse(route, status, cause) {
993
+ return new ApiError({
994
+ code: "bad_response",
995
+ message: `${route} answered with a body the SDK cannot use${cause === void 0 ? "" : ` (${describeError(cause)})`}.`,
996
+ status,
997
+ cause
998
+ });
999
+ }
1000
+ async function guarded(call, route) {
1001
+ try {
1002
+ return await call;
1003
+ } catch (error) {
1004
+ if (error instanceof HushscriptError) throw error;
1005
+ throw badResponse(route, null, error);
1006
+ }
1007
+ }
1008
+ function usable(json) {
1009
+ return nestsDeeperThan(json, MAX_BODY_DEPTH) ? void 0 : json;
1010
+ }
1011
+ async function readJson(call, route) {
1012
+ const { data, response } = await guarded(call, route);
1013
+ try {
1014
+ return { status: response.status, json: usable(JSON.parse(String(data))) };
1015
+ } catch {
1016
+ return { status: response.status, json: void 0 };
1017
+ }
1018
+ }
1019
+ async function readBody(call, route, shape) {
1020
+ const { status, json } = await readJson(call, route);
1021
+ const found = json === void 0 ? { path: [], expected: "a JSON body" } : shape(json);
1022
+ if (found !== null) throw badResponse(route, status, new Error(describeProblem(found)));
1023
+ return json;
1024
+ }
1025
+ async function readObject(call, route) {
1026
+ const { status, json } = await readJson(call, route);
1027
+ const isObject = typeof json === "object" && !Array.isArray(json);
1028
+ return { status, body: isObject ? json : null };
1029
+ }
1030
+
1031
+ // src/http/guard.ts
1032
+ var VERBS = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"];
1033
+ function guardApi(raw) {
1034
+ const guarded2 = {
1035
+ use: raw.use.bind(raw),
1036
+ eject: raw.eject.bind(raw)
1037
+ };
1038
+ const guard = (name, call) => {
1039
+ const routeAt = name === "request" ? 1 : 0;
1040
+ guarded2[name] = async (...args) => {
1041
+ try {
1042
+ return await call(...args);
1043
+ } catch (error) {
1044
+ if (error instanceof HushscriptError) throw error;
1045
+ const verb = name === "request" ? String(args[0]).toUpperCase() : name;
1046
+ throw badResponse(`${verb} ${String(args[routeAt])}`, null, error);
1047
+ }
1048
+ };
1049
+ };
1050
+ for (const verb of VERBS) guard(verb, raw[verb]);
1051
+ guard("request", raw.request);
1052
+ return guarded2;
1053
+ }
1054
+
1055
+ // src/http/fetch.ts
1056
+ var BODY_CHUNK_BYTES = 256 * 1024;
1057
+ var NEVER_SENT = /^(?:ECONNREFUSED|ENOTFOUND|EAI_AGAIN|UND_ERR_CONNECT_TIMEOUT|ERR_TLS_CERT_ALTNAME_INVALID|CERT_|DEPTH_ZERO_|UNABLE_TO_|SELF_SIGNED_)/;
1058
+ function isTransportFailure(error) {
1059
+ return error instanceof TypeError || error instanceof DOMException && error.name === "NetworkError";
1060
+ }
1061
+ function errorCode(error) {
1062
+ let current = error;
1063
+ for (let depth = 0; depth < 4 && current instanceof Error; depth += 1) {
1064
+ const code = current.code;
1065
+ if (typeof code === "string") return code;
1066
+ current = current.cause;
1067
+ }
1068
+ return void 0;
1069
+ }
1070
+ function bodyStream(bytes, progress) {
1071
+ let offset = 0;
1072
+ return new ReadableStream(
1073
+ {
1074
+ pull(controller) {
1075
+ const end = Math.min(offset + BODY_CHUNK_BYTES, bytes.byteLength);
1076
+ controller.enqueue(bytes.subarray(offset, end));
1077
+ offset = end;
1078
+ if (offset >= bytes.byteLength) {
1079
+ progress.done = true;
1080
+ controller.close();
1081
+ }
1082
+ }
1083
+ },
1084
+ { highWaterMark: 0 }
1085
+ );
1086
+ }
1087
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 205, 304]);
1088
+ function whenAborted(signal) {
1089
+ return new Promise((_resolve, reject) => {
1090
+ if (signal.aborted) reject(signal.reason);
1091
+ else
1092
+ signal.addEventListener("abort", () => {
1093
+ reject(signal.reason);
1094
+ });
1095
+ });
1096
+ }
1097
+ function keepHugeRetryAfter(key, value, context) {
1098
+ if (key === "retry_after_seconds" && value === Infinity && // Stryker disable next-line ConditionalExpression,OptionalChaining: every Node under test passes the source; the branch is for runtimes that do not
1099
+ context?.source !== void 0 && /^\d+$/.test(context.source))
1100
+ return MAX_RETRY_AFTER_REPORTED;
1101
+ return value;
1102
+ }
1103
+ function parseBody(text) {
1104
+ try {
1105
+ const parsed = JSON.parse(text, keepHugeRetryAfter);
1106
+ return nestsDeeperThan(parsed, MAX_BODY_DEPTH) ? text : parsed;
1107
+ } catch {
1108
+ return text;
1109
+ }
1110
+ }
1111
+ function slugOf(body) {
1112
+ if (body === null) return null;
1113
+ if (typeof body !== "object") return null;
1114
+ const slug = body.error;
1115
+ return typeof slug === "string" ? slug : null;
1116
+ }
1117
+ function isAborted(signal) {
1118
+ return signal.aborted;
1119
+ }
1120
+ function aborted2(signal) {
1121
+ return new AbortError({ message: "The request was aborted.", cause: signal.reason });
1122
+ }
1123
+ async function pause(cfg, ms, signal) {
1124
+ try {
1125
+ await cfg.clock.sleep(ms, signal);
1126
+ } catch (error) {
1127
+ if (signal.aborted) throw aborted2(signal);
1128
+ throw error;
1129
+ }
1130
+ }
1131
+ async function attempt(timeoutMs, cfg, request, callerSignal) {
1132
+ const controller = new AbortController();
1133
+ const stopTimer = new AbortController();
1134
+ const onCallerAbort = () => {
1135
+ controller.abort(callerSignal.reason);
1136
+ };
1137
+ callerSignal.addEventListener("abort", onCallerAbort);
1138
+ let timedOut = false;
1139
+ cfg.clock.sleep(timeoutMs, stopTimer.signal).then(
1140
+ () => {
1141
+ timedOut = true;
1142
+ controller.abort(new Error("timeout"));
1143
+ },
1144
+ () => void 0
1145
+ );
1146
+ const progress = { done: request.bytes === null };
1147
+ let headersArrived = false;
1148
+ try {
1149
+ const response = await globalThis.fetch(request.url, {
1150
+ method: request.method,
1151
+ headers: request.headers,
1152
+ body: request.bytes === null ? null : bodyStream(request.bytes, progress),
1153
+ duplex: "half",
1154
+ // Never followed: a 3xx is an error, and credentials never leave the API origin.
1155
+ redirect: "manual",
1156
+ signal: controller.signal
1157
+ });
1158
+ headersArrived = true;
1159
+ const bytes = NULL_BODY_STATUSES.has(response.status) ? null : await Promise.race([response.arrayBuffer(), whenAborted(controller.signal)]);
1160
+ return {
1161
+ response: new Response(bytes, {
1162
+ status: response.status,
1163
+ statusText: response.statusText,
1164
+ headers: response.headers
1165
+ })
1166
+ };
1167
+ } catch (error) {
1168
+ const connectPhase = NEVER_SENT.test(errorCode(error) ?? "");
1169
+ const sent = headersArrived || !connectPhase && progress.done;
1170
+ const foreign = !controller.signal.aborted && !isTransportFailure(error);
1171
+ return { failure: { error, sent, timedOut, foreign } };
1172
+ } finally {
1173
+ stopTimer.abort();
1174
+ callerSignal.removeEventListener("abort", onCallerAbort);
1175
+ }
1176
+ }
1177
+ function terminalFailure(timeoutMs, retryClass, operation, failure, callerSignal) {
1178
+ const code = errorCode(failure.error);
1179
+ const callerAborted = isAborted(callerSignal);
1180
+ const reason = callerAborted ? "The request was aborted." : failure.timedOut ? `The request timed out after ${String(timeoutMs / 1e3)} s.` : `The request failed before a response arrived (${describeError(failure.error)}${code === void 0 ? "" : `; code ${code}`}).`;
1181
+ if (retryClass === "never" && failure.sent) {
1182
+ return new OutcomeUnknownError({
1183
+ message: `${reason} The request was sent, so its outcome is unknown.`,
1184
+ hint: OutcomeUnknownError.hintFor(operation),
1185
+ cause: callerAborted ? aborted2(callerSignal) : failure.error
1186
+ });
1187
+ }
1188
+ if (callerAborted) return aborted2(callerSignal);
1189
+ return new NetworkError({ message: reason, cause: failure.error });
1190
+ }
1191
+ function hardenedFetch(cfg, retryClass, options = {}) {
1192
+ const timeoutMs = options.timeoutMs ?? cfg.timeoutMs;
1193
+ const guarded2 = async (input, init, wire) => {
1194
+ const direct = init?.body instanceof Uint8Array ? init.body : null;
1195
+ const request = new Request(input, direct === null ? init : { ...init, body: null });
1196
+ const url2 = new URL(request.url);
1197
+ const method = request.method.toUpperCase();
1198
+ const operation = operationFor(method, url2.pathname);
1199
+ const apiOrigin = new URL(cfg.baseUrl).origin;
1200
+ const target = describeTarget(url2, apiOrigin);
1201
+ const signal = request.signal;
1202
+ const buffer = direct ?? new Uint8Array(await request.arrayBuffer());
1203
+ const bytes = buffer.byteLength > 0 ? buffer : null;
1204
+ const headers = new Headers(request.headers);
1205
+ if (url2.origin !== apiOrigin) headers.delete("authorization");
1206
+ const carriesToken = headers.has("authorization");
1207
+ if (bytes !== null) headers.set("content-length", String(bytes.byteLength));
1208
+ const prepared = { url: request.url, method, headers, bytes };
1209
+ let rateLimitsDone = 0;
1210
+ let activeJobsDone = 0;
1211
+ for (let retriesDone = 0; ; retriesDone += 1) {
1212
+ wire.sent = false;
1213
+ if (signal.aborted) throw aborted2(signal);
1214
+ if (operation === "POST /v1/uploads")
1215
+ await cfg.createPacer.acquire(signal).catch(() => {
1216
+ throw aborted2(signal);
1217
+ });
1218
+ if (carriesToken) prepared.headers.set("authorization", cfg.authorization());
1219
+ const startedAt = cfg.clock.now();
1220
+ const outcome = await attempt(timeoutMs, cfg, prepared, signal);
1221
+ wire.sent = "response" in outcome || outcome.failure.sent;
1222
+ wire.everSent ||= wire.sent;
1223
+ const durationMs = cfg.clock.now() - startedAt;
1224
+ const log = (status, retryDelayMs) => {
1225
+ cfg.logger.attempt({
1226
+ method,
1227
+ target,
1228
+ status,
1229
+ attempt: retriesDone + 1,
1230
+ durationMs,
1231
+ retryDelayMs
1232
+ });
1233
+ };
1234
+ if ("response" in outcome) {
1235
+ const response = outcome.response;
1236
+ if (response.ok) {
1237
+ log(response.status, null);
1238
+ return response;
1239
+ }
1240
+ const body = parseBody(await response.text());
1241
+ const plan2 = planRetry({
1242
+ retryClass,
1243
+ status: response.status,
1244
+ // Stryker disable next-line BooleanLiteral: planRetry reads `sent` only when there is no status
1245
+ sent: true,
1246
+ slug: slugOf(body),
1247
+ retryAfter: parseRetryAfter(response.headers.get("retry-after")),
1248
+ retriesDone,
1249
+ rateLimitsDone,
1250
+ activeJobsDone,
1251
+ maxRetries: cfg.maxRetries,
1252
+ random: cfg.random
1253
+ });
1254
+ if (response.status === 429) {
1255
+ if (slugOf(body) === "too_many_active_jobs") activeJobsDone += 1;
1256
+ else rateLimitsDone += 1;
1257
+ }
1258
+ log(response.status, plan2.retry ? plan2.delayMs : null);
1259
+ if (!plan2.retry) throw errorFromResponse(response, body, operation);
1260
+ await pause(cfg, plan2.delayMs, signal);
1261
+ continue;
1262
+ }
1263
+ const failure = outcome.failure;
1264
+ if (failure.foreign) {
1265
+ log("unexpected_error", null);
1266
+ throw failure.error;
1267
+ }
1268
+ if (isAborted(signal))
1269
+ throw terminalFailure(timeoutMs, retryClass, operation, failure, signal);
1270
+ const plan = planRetry({
1271
+ retryClass,
1272
+ status: null,
1273
+ sent: failure.sent,
1274
+ slug: null,
1275
+ retryAfter: null,
1276
+ retriesDone,
1277
+ rateLimitsDone,
1278
+ activeJobsDone,
1279
+ maxRetries: cfg.maxRetries,
1280
+ random: cfg.random
1281
+ });
1282
+ log(failure.timedOut ? "timeout" : "network_error", plan.retry ? plan.delayMs : null);
1283
+ if (!plan.retry) throw terminalFailure(timeoutMs, retryClass, operation, failure, signal);
1284
+ await pause(cfg, plan.delayMs, signal);
1285
+ }
1286
+ };
1287
+ return async (input, init) => {
1288
+ const wire = { sent: false, everSent: false };
1289
+ try {
1290
+ return await guarded2(input, init, wire);
1291
+ } catch (error) {
1292
+ if (error instanceof HushscriptError) throw wire.everSent ? error : markNotSent(error);
1293
+ if (retryClass === "never" && wire.sent) {
1294
+ throw new OutcomeUnknownError({
1295
+ message: `The SDK failed unexpectedly after the request was sent (${describeError(error)}), so its outcome is unknown.`,
1296
+ cause: error
1297
+ });
1298
+ }
1299
+ const wrapped = asHushscriptError(error, wire.everSent);
1300
+ throw wire.everSent ? wrapped : markNotSent(wrapped);
1301
+ }
1302
+ };
1303
+ }
1304
+
1305
+ // src/http/transport.ts
1306
+ function createTransport(config) {
1307
+ return async (input, init) => {
1308
+ const request = new Request(input, init);
1309
+ const pathname = new URL(request.url).pathname;
1310
+ const body = request.method.toUpperCase() === "POST" && pathname === "/v1/uploads" ? await request.clone().text() : null;
1311
+ return hardenedFetch(config, retryClassForRequest(request.method, pathname, body))(request);
1312
+ };
1313
+ }
1314
+
1315
+ // src/money.ts
1316
+ var NO_MINOR_UNITS = /* @__PURE__ */ new Set([
1317
+ "BIF",
1318
+ "CLP",
1319
+ "DJF",
1320
+ "GNF",
1321
+ "JPY",
1322
+ "KMF",
1323
+ "KRW",
1324
+ "MGA",
1325
+ "PYG",
1326
+ "RWF",
1327
+ "UGX",
1328
+ "VND",
1329
+ "VUV",
1330
+ "XAF",
1331
+ "XOF",
1332
+ "XPF"
1333
+ ]);
1334
+ var THREE_MINOR_UNITS = /* @__PURE__ */ new Set(["BHD", "JOD", "KWD", "OMR", "TND"]);
1335
+ function minorUnitDigits(currency) {
1336
+ const code = currency.toUpperCase();
1337
+ if (NO_MINOR_UNITS.has(code)) return 0;
1338
+ return THREE_MINOR_UNITS.has(code) ? 3 : 2;
1339
+ }
1340
+ var KNOWN_CURRENCIES = new Set(Intl.supportedValuesOf("currency"));
1341
+ function display(amount, currency, digits) {
1342
+ const code = currency.toUpperCase();
1343
+ if (!KNOWN_CURRENCIES.has(code)) return `${(amount / 10 ** digits).toFixed(digits)} ${code}`;
1344
+ const format = new Intl.NumberFormat("en", {
1345
+ style: "currency",
1346
+ currency: code,
1347
+ minimumFractionDigits: digits,
1348
+ maximumFractionDigits: digits
1349
+ });
1350
+ return format.format(amount / 10 ** digits);
1351
+ }
1352
+ function toMoney(amount, currency) {
1353
+ return { amount, currency, formatted: display(amount, currency, minorUnitDigits(currency)) };
1354
+ }
1355
+
1356
+ // src/shapes.ts
1357
+ var UPLOAD_LIMITS = objectOf({
1358
+ max_daily_seconds: num,
1359
+ max_active_jobs: num,
1360
+ active_jobs: num,
1361
+ used_today_seconds: num
1362
+ });
1363
+ var ACCOUNT = objectOf({
1364
+ balance_seconds: num,
1365
+ held_seconds: num,
1366
+ credits_expire_at: nullable(timestamp),
1367
+ card_on_file: bool,
1368
+ data_region: str,
1369
+ limits: UPLOAD_LIMITS
1370
+ });
1371
+ var OPTIONS_SUMMARY = objectOf({
1372
+ language_code: nullable(str),
1373
+ speaker_detection: bool,
1374
+ medical_mode: bool,
1375
+ multichannel: bool,
1376
+ translation_targets: arrayOf(str),
1377
+ keyterms: bool,
1378
+ prompt: bool,
1379
+ auto_generate_insights: nullable(bool)
1380
+ });
1381
+ var JOB = objectOf({
1382
+ id: str,
1383
+ state: str,
1384
+ mode: str,
1385
+ declared_seconds: nullable(num),
1386
+ verified_seconds: nullable(num),
1387
+ actual_seconds: nullable(num),
1388
+ billable_seconds: nullable(num),
1389
+ error: nullable(str),
1390
+ failure_kind: nullable(str),
1391
+ retry_of: nullable(str),
1392
+ created_at: timestamp,
1393
+ transcript_id: nullable(pathId),
1394
+ options: OPTIONS_SUMMARY
1395
+ });
1396
+ var JOB_LIST = objectOf({
1397
+ jobs: arrayOf(JOB),
1398
+ next_cursor: nullable(str)
1399
+ });
1400
+ var JOB_ONE = JOB;
1401
+ var MAX_PART_BYTES = 32 * 1024 * 1024;
1402
+ var PART_URL = objectOf({ n: positiveInt, url });
1403
+ var UPLOAD_CREATE = objectOf({
1404
+ job_id: pathId,
1405
+ state: str,
1406
+ upload: nullable(
1407
+ objectOf({
1408
+ part_size_bytes: positiveIntAtMost(MAX_PART_BYTES),
1409
+ total_parts: positiveInt,
1410
+ part_urls: nullable(arrayOf(PART_URL)),
1411
+ next_part_number: nullable(positiveInt)
1412
+ })
1413
+ )
1414
+ });
1415
+ var UPLOAD_COMPLETE = objectOf({ state: str });
1416
+ var PART_URLS = nullable(
1417
+ objectOf({
1418
+ part_urls: nullable(arrayOf(PART_URL)),
1419
+ next_part_number: optional(nullable(positiveInt))
1420
+ })
1421
+ );
1422
+ var UTTERANCE = objectOf({
1423
+ speaker: str,
1424
+ start_ms: num,
1425
+ end_ms: num,
1426
+ text: str
1427
+ });
1428
+ var TRANSCRIPT_LANGUAGE = objectOf({
1429
+ utterances: arrayOf(UTTERANCE)
1430
+ });
1431
+ var TRANSCRIPT_DETAIL = objectOf({
1432
+ id: str,
1433
+ title: str,
1434
+ language_detected: nullable(str),
1435
+ duration_seconds: num,
1436
+ speaker_names: recordOf(str),
1437
+ tags: arrayOf(str),
1438
+ created_at: timestamp,
1439
+ updated_at: timestamp,
1440
+ auto_delete_at: nullable(timestamp),
1441
+ edited: bool,
1442
+ body: objectOf({
1443
+ original: TRANSCRIPT_LANGUAGE,
1444
+ translations: recordOf(TRANSCRIPT_LANGUAGE)
1445
+ })
1446
+ });
1447
+ var TRANSCRIPT_LIST = objectOf({
1448
+ transcripts: arrayOf(
1449
+ objectOf({
1450
+ id: str,
1451
+ title: str,
1452
+ language_detected: nullable(str),
1453
+ duration_seconds: num,
1454
+ created_at: timestamp,
1455
+ auto_delete_at: nullable(timestamp),
1456
+ tags: arrayOf(str)
1457
+ })
1458
+ ),
1459
+ next_cursor: nullable(str)
1460
+ });
1461
+ var DELETED_LIST = objectOf({
1462
+ deleted: arrayOf(
1463
+ objectOf({
1464
+ id: str,
1465
+ title: str,
1466
+ duration_seconds: num,
1467
+ created_at: timestamp,
1468
+ deleted_at: timestamp,
1469
+ cause: str,
1470
+ receipt: nullable(recordOf(anything))
1471
+ })
1472
+ ),
1473
+ next_cursor: nullable(str)
1474
+ });
1475
+ var PACKS = objectOf({
1476
+ packs: arrayOf(
1477
+ objectOf({
1478
+ id: str,
1479
+ amount: num,
1480
+ currency: str,
1481
+ seconds: num,
1482
+ referral_bonus_seconds: num,
1483
+ available: optional(bool)
1484
+ })
1485
+ )
1486
+ });
1487
+ var CARDS = objectOf({
1488
+ default_id: nullable(str),
1489
+ methods: arrayOf(
1490
+ objectOf({
1491
+ id: str,
1492
+ type: str,
1493
+ brand: str,
1494
+ last4: str,
1495
+ exp_month: num,
1496
+ exp_year: num,
1497
+ email: nullable(str),
1498
+ is_default: bool,
1499
+ fast_checkout_eligible: bool
1500
+ })
1501
+ )
1502
+ });
1503
+ var SET_DEFAULT_CARD = objectOf({ ok: bool });
1504
+ var SETUP_SESSION = objectOf({
1505
+ checkout_url: url
1506
+ });
1507
+ var TRANSACTIONS = objectOf({
1508
+ transactions: arrayOf(
1509
+ objectOf({
1510
+ id: num,
1511
+ seconds: num,
1512
+ amount_cents: num,
1513
+ amount_refunded_cents: num,
1514
+ currency: str,
1515
+ status: str,
1516
+ created_at: timestamp,
1517
+ payment_method_type: nullable(str),
1518
+ payment_method_brand: nullable(str),
1519
+ payment_method_last4: nullable(str),
1520
+ receipt_number: nullable(str),
1521
+ initiated_via: nullable(str)
1522
+ })
1523
+ ),
1524
+ next_cursor: nullable(str)
1525
+ });
1526
+ var LEDGER = objectOf({
1527
+ entries: arrayOf(
1528
+ objectOf({
1529
+ id: num,
1530
+ delta_seconds: num,
1531
+ kind: str,
1532
+ job_id: nullable(str),
1533
+ note: nullable(str),
1534
+ created_at: timestamp
1535
+ })
1536
+ ),
1537
+ next_cursor: nullable(str)
1538
+ });
1539
+ var HOLDS = objectOf({
1540
+ holds: arrayOf(
1541
+ objectOf({
1542
+ id: str,
1543
+ kind: str,
1544
+ seconds: num,
1545
+ created_at: timestamp
1546
+ })
1547
+ ),
1548
+ next_cursor: nullable(str)
1549
+ });
1550
+ var AGENT_SIGNUP = objectOf({
1551
+ signup_id: str,
1552
+ claim_secret: str,
1553
+ checkout_url: str,
1554
+ amount: num,
1555
+ currency: str,
1556
+ expires_at: timestamp
1557
+ });
1558
+
1559
+ // src/resources/account.ts
1560
+ var ACCOUNT_FIELDS = /* @__PURE__ */ new Set([
1561
+ ...[
1562
+ "balance_seconds",
1563
+ "held_seconds",
1564
+ "credits_expire_at",
1565
+ "card_on_file",
1566
+ "data_region",
1567
+ "limits"
1568
+ ],
1569
+ "purchases_remaining_in_window"
1570
+ ]);
1571
+ function expiry(raw) {
1572
+ return raw.credits_expire_at === null ? null : new Date(raw.credits_expire_at * 1e3);
1573
+ }
1574
+ function purchasesRemaining(raw) {
1575
+ const remaining = raw["purchases_remaining_in_window"];
1576
+ return typeof remaining === "number" && Number.isSafeInteger(remaining) && remaining >= 0 ? remaining : null;
1577
+ }
1578
+ function toAccount(raw) {
1579
+ return {
1580
+ ...unknownFields(raw, ACCOUNT_FIELDS),
1581
+ balanceSeconds: raw.balance_seconds,
1582
+ heldSeconds: raw.held_seconds,
1583
+ creditsExpireAt: expiry(raw),
1584
+ cardOnFile: raw.card_on_file,
1585
+ dataRegion: raw.data_region,
1586
+ limits: camelize(raw.limits),
1587
+ purchasesRemainingInWindow: purchasesRemaining(raw)
1588
+ };
1589
+ }
1590
+ function toBalance(raw) {
1591
+ return {
1592
+ ...unknownFields(raw, ACCOUNT_FIELDS),
1593
+ seconds: raw.balance_seconds,
1594
+ // `+ 0` turns the -0 that truncating -59 / 60 gives into 0.
1595
+ minutes: Math.trunc(raw.balance_seconds / 60) + 0,
1596
+ heldSeconds: raw.held_seconds,
1597
+ expiresAt: expiry(raw),
1598
+ purchasesRemaining: purchasesRemaining(raw)
1599
+ };
1600
+ }
1601
+ function readAccount(api, signal) {
1602
+ return readBody(
1603
+ api.GET("/v1/account", { parseAs: "text", ...signal === void 0 ? {} : { signal } }),
1604
+ "GET /v1/account",
1605
+ ACCOUNT
1606
+ );
1607
+ }
1608
+ var ID3_HEADER_BYTES = 10;
1609
+ var MAX_ID3_TAGS = 4;
1610
+ var FRAME_SCAN_BYTES = 4096;
1611
+ var OPUS_SAMPLE_RATE = 48e3;
1612
+ var OPUS_HEAD_BYTES = 512;
1613
+ var OPUS_PRE_SKIP_AT = 10;
1614
+ async function opusPreSkipSeconds(source, signal) {
1615
+ const head = await source.head(OPUS_HEAD_BYTES, signal);
1616
+ const at = ascii(head, 0, head.length).indexOf("OpusHead");
1617
+ if (at === -1 || at + OPUS_PRE_SKIP_AT + 2 > head.length) return 0;
1618
+ const preSkip = (head[at + OPUS_PRE_SKIP_AT] ?? 0) | (head[at + OPUS_PRE_SKIP_AT + 1] ?? 0) << 8;
1619
+ return preSkip / OPUS_SAMPLE_RATE;
1620
+ }
1621
+ var ProbeBlob = class _ProbeBlob extends Blob {
1622
+ size;
1623
+ source;
1624
+ signal;
1625
+ offset;
1626
+ length;
1627
+ constructor(source, signal, offset = 0, length = source.size) {
1628
+ super([]);
1629
+ this.size = length;
1630
+ this.source = source;
1631
+ this.signal = signal;
1632
+ this.offset = offset;
1633
+ this.length = length;
1634
+ }
1635
+ slice(start = 0, end = this.size) {
1636
+ const clamp = (at) => Math.min(this.size, Math.max(0, at < 0 ? this.size + at : at));
1637
+ const from = clamp(start);
1638
+ return new _ProbeBlob(
1639
+ this.source,
1640
+ this.signal,
1641
+ this.offset + from,
1642
+ Math.max(0, clamp(end) - from)
1643
+ );
1644
+ }
1645
+ async arrayBuffer() {
1646
+ if (this.signal?.aborted) throw aborted(this.signal);
1647
+ const bytes = await this.source.read(this.offset, this.length, this.signal);
1648
+ await setImmediate();
1649
+ if (this.signal?.aborted) throw aborted(this.signal);
1650
+ return bytes.slice().buffer;
1651
+ }
1652
+ };
1653
+ async function parse(source, duration, signal) {
1654
+ await setImmediate();
1655
+ if (signal?.aborted) throw aborted(signal);
1656
+ const metadata = await parseBlob(new ProbeBlob(source, signal), {
1657
+ duration,
1658
+ skipCovers: true
1659
+ });
1660
+ if (signal?.aborted) throw aborted(signal);
1661
+ return metadata;
1662
+ }
1663
+ function syncsafe(bytes, at) {
1664
+ return ((bytes[at] ?? 0) & 127) * 2 ** 21 + ((bytes[at + 1] ?? 0) & 127) * 2 ** 14 + ((bytes[at + 2] ?? 0) & 127) * 2 ** 7 + ((bytes[at + 3] ?? 0) & 127);
1665
+ }
1666
+ function ascii(bytes, at, length) {
1667
+ return String.fromCharCode(...bytes.subarray(at, at + length));
1668
+ }
1669
+ async function firstFrame(source, signal) {
1670
+ let at = 0;
1671
+ for (let tags = 0; tags < MAX_ID3_TAGS; tags += 1) {
1672
+ const header = await source.read(at, ID3_HEADER_BYTES, signal);
1673
+ if (ascii(header, 0, 3) !== "ID3" || header.length < ID3_HEADER_BYTES) break;
1674
+ const footer = ((header[5] ?? 0) & 16) === 0 ? 0 : 10;
1675
+ at += ID3_HEADER_BYTES + syncsafe(header, 6) + footer;
1676
+ }
1677
+ const window = await source.read(at, FRAME_SCAN_BYTES, signal);
1678
+ for (let i = 0; i + 4 <= window.length; i += 1) {
1679
+ const b1 = window[i + 1] ?? 0;
1680
+ if (window[i] === 255 && (b1 & 224) === 224 && (b1 & 6) !== 0 && (b1 & 24) !== 8) {
1681
+ return window.subarray(i);
1682
+ }
1683
+ }
1684
+ return null;
1685
+ }
1686
+ async function hasDurationHeader(source, signal) {
1687
+ const frame = await firstFrame(source, signal);
1688
+ if (frame === null) return false;
1689
+ const b1 = frame[1] ?? 0;
1690
+ const mpeg1 = (b1 & 24) === 24;
1691
+ const mono = (frame[3] ?? 0) >> 6 === 3;
1692
+ const crc = (b1 & 1) === 0 ? 2 : 0;
1693
+ const sideInfo = mpeg1 ? mono ? 17 : 32 : mono ? 9 : 17;
1694
+ const tag = ascii(frame, 4 + crc + sideInfo, 4);
1695
+ return tag === "Xing" || tag === "Info" || ascii(frame, 4 + 32, 4) === "VBRI";
1696
+ }
1697
+ function unreadable(cause) {
1698
+ return new UnsupportedMediaError({
1699
+ code: "duration_unreadable",
1700
+ message: `Could not read the duration${cause === void 0 ? "" : ` (${describeError(cause)})`}; pass durationSeconds.`,
1701
+ cause
1702
+ });
1703
+ }
1704
+ async function probeDuration(source, signal) {
1705
+ if (signal?.aborted) throw aborted(signal);
1706
+ if (source.probeInput === null) throw unreadable();
1707
+ let duration;
1708
+ try {
1709
+ let meta = await parse(source, false, signal);
1710
+ if (meta.format.container === "MPEG" && !await hasDurationHeader(source, signal) || meta.format.duration === void 0) {
1711
+ meta = await parse(source, true, signal);
1712
+ }
1713
+ duration = meta.format.duration ?? NaN;
1714
+ if (/opus/i.test(meta.format.codec ?? "")) duration += await opusPreSkipSeconds(source, signal);
1715
+ } catch (error) {
1716
+ if (signal?.aborted) throw aborted(signal);
1717
+ if (error instanceof AbortError) throw error;
1718
+ throw unreadable(error);
1719
+ }
1720
+ if (signal?.aborted) throw aborted(signal);
1721
+ if (!Number.isFinite(duration) || duration <= 0) throw unreadable();
1722
+ return duration;
1723
+ }
1724
+
1725
+ // src/transcribe/sniff.ts
1726
+ function startsWith(head, text, offset = 0) {
1727
+ for (let i = 0; i < text.length; i += 1) {
1728
+ if (head[offset + i] !== text.charCodeAt(i)) return false;
1729
+ }
1730
+ return true;
1731
+ }
1732
+ function isLayer3Sync(head) {
1733
+ const [a, b = 0] = head;
1734
+ return a === 255 && (b & 224) === 224 && (b & 6) === 2;
1735
+ }
1736
+ function sniff(head) {
1737
+ if (startsWith(head, "ID3")) return "mp3";
1738
+ if (head.length >= 12 && startsWith(head, "ftyp", 4)) return "isobmff";
1739
+ if ((startsWith(head, "RIFF") || startsWith(head, "RF64") || startsWith(head, "BW64")) && startsWith(head, "WAVE", 8)) {
1740
+ return "wav";
1741
+ }
1742
+ if (startsWith(head, "fLaC")) return "flac";
1743
+ if (startsWith(head, "OggS")) return "ogg";
1744
+ return isLayer3Sync(head) ? "mp3" : null;
1745
+ }
1746
+
1747
+ // src/transcribe/preflight.ts
1748
+ var MAX_FILE_BYTES = 5e9;
1749
+ var MIN_DURATION_SECONDS = 15;
1750
+ var MAX_DURATION_SECONDS = 36e3;
1751
+ var MAX_KEYTERMS = 1e3;
1752
+ var MAX_KEYTERM_WORDS = 6;
1753
+ var MAX_KEYTERM_CHARS = 128;
1754
+ var MAX_PROMPT_CHARS = 2e3;
1755
+ var COMPILED_PROMPT_OVERHEAD = 203;
1756
+ var MAX_MATTER_CODE = 64;
1757
+ var MEDICAL_LANGUAGES = ["en", "es", "fr", "de"];
1758
+ var SNIFF_BYTES = 64;
1759
+ var WHITE_SPACE_EDGES = new RegExp("^\\p{White_Space}+|\\p{White_Space}+$", "gu");
1760
+ var WHITE_SPACE_RUN = new RegExp("\\p{White_Space}+", "u");
1761
+ function codePoints(text) {
1762
+ return Array.from(text).length;
1763
+ }
1764
+ function trimWhiteSpace(text) {
1765
+ return text.replace(WHITE_SPACE_EDGES, "");
1766
+ }
1767
+ function normalizeContext(options) {
1768
+ const keyterms = [];
1769
+ const keytermIndexes = [];
1770
+ (options.keyterms ?? []).forEach((term, index) => {
1771
+ const trimmed = trimWhiteSpace(term);
1772
+ if (trimmed !== "") {
1773
+ keyterms.push(trimmed);
1774
+ keytermIndexes.push(index);
1775
+ }
1776
+ });
1777
+ const prompt = trimWhiteSpace(options.prompt ?? "");
1778
+ return { keyterms, keytermIndexes, prompt: prompt === "" ? void 0 : prompt };
1779
+ }
1780
+ function checkContext(options) {
1781
+ const { keyterms, keytermIndexes, prompt } = normalizeContext(options);
1782
+ const unique = new Set(keyterms);
1783
+ if (unique.size > MAX_KEYTERMS) {
1784
+ throw invalidOption(
1785
+ `keyterms takes at most ${String(MAX_KEYTERMS)} unique terms (${String(unique.size)} given).`
1786
+ );
1787
+ }
1788
+ keyterms.forEach((term, position) => {
1789
+ const name = `keyterms[${String(keytermIndexes[position])}]`;
1790
+ if (codePoints(term) > MAX_KEYTERM_CHARS) {
1791
+ throw invalidOption(`${name} is longer than ${String(MAX_KEYTERM_CHARS)} characters.`);
1792
+ }
1793
+ if (term.split(WHITE_SPACE_RUN).length > MAX_KEYTERM_WORDS) {
1794
+ throw invalidOption(`${name} has more than ${String(MAX_KEYTERM_WORDS)} words.`);
1795
+ }
1796
+ });
1797
+ if (prompt === void 0) return;
1798
+ const promptChars = codePoints(prompt);
1799
+ if (promptChars > MAX_PROMPT_CHARS) {
1800
+ throw invalidOption(`prompt is longer than ${String(MAX_PROMPT_CHARS)} characters.`);
1801
+ }
1802
+ if (unique.size > 0) {
1803
+ let termChars = unique.size - 1;
1804
+ for (const term of unique) termChars += codePoints(term) + 2;
1805
+ const compiled = COMPILED_PROMPT_OVERHEAD + promptChars + termChars;
1806
+ if (compiled > MAX_PROMPT_CHARS) {
1807
+ throw invalidOption(
1808
+ `prompt and keyterms together exceed ${String(MAX_PROMPT_CHARS)} characters (${String(compiled)}).`
1809
+ );
1810
+ }
1811
+ }
1812
+ }
1813
+ function invalidOption(message) {
1814
+ return new PreflightError({ code: "invalid_option", message });
1815
+ }
1816
+ function checkPathId(id, name) {
1817
+ if (!isPathSegment(id)) {
1818
+ throw invalidOption(
1819
+ `${name} must be a non-empty string that is not "." or "..", with no lone surrogate.`
1820
+ );
1821
+ }
1822
+ return id;
1823
+ }
1824
+ function checkDurationOption(seconds2) {
1825
+ if (seconds2 !== void 0 && !(Number.isFinite(seconds2) && seconds2 > 0)) {
1826
+ throw invalidOption("durationSeconds must be a finite number of seconds, above 0.");
1827
+ }
1828
+ }
1829
+ function checkIdempotencyKey(key) {
1830
+ let printable = typeof key === "string" && key.length >= 16 && key.length <= 128;
1831
+ for (let i = 0; printable && i < key.length; i += 1) {
1832
+ const code = key.charCodeAt(i);
1833
+ printable = code >= 33 && code <= 126;
1834
+ }
1835
+ if (!printable) {
1836
+ throw invalidOption(
1837
+ "idempotencyKey must be 16 to 128 printable ASCII characters, without spaces."
1838
+ );
1839
+ }
1840
+ }
1841
+ function checkOptions(options) {
1842
+ if (options.medical === true && !MEDICAL_LANGUAGES.includes(options.language ?? "")) {
1843
+ throw invalidOption("medical needs language to be one of en, es, fr or de.");
1844
+ }
1845
+ checkContext(options);
1846
+ if ((options.matterCode ?? "").length > MAX_MATTER_CODE) {
1847
+ throw invalidOption(`matterCode is at most ${String(MAX_MATTER_CODE)} characters.`);
1848
+ }
1849
+ if (options.idempotencyKey !== void 0) checkIdempotencyKey(options.idempotencyKey);
1850
+ }
1851
+ async function readHead(source, signal) {
1852
+ try {
1853
+ return await source.head(SNIFF_BYTES, signal);
1854
+ } catch (error) {
1855
+ if (error instanceof HushscriptError) throw error;
1856
+ throw new PreflightError({
1857
+ code: "file_unreadable",
1858
+ message: `The input could not be read (${describeError(error)}).`,
1859
+ cause: error
1860
+ });
1861
+ }
1862
+ }
1863
+ async function preflight(source, options, api, signal) {
1864
+ if (sniff(await readHead(source, signal)) === null) {
1865
+ throw new UnsupportedMediaError({
1866
+ code: "unsupported_media",
1867
+ message: "The file is not in a supported audio format."
1868
+ });
1869
+ }
1870
+ if (source.size > MAX_FILE_BYTES) {
1871
+ throw new FileLimitError({
1872
+ code: "size_out_of_range",
1873
+ message: `The file is ${String(source.size)} bytes; the limit is 1 byte to ${String(MAX_FILE_BYTES)} bytes.`
1874
+ });
1875
+ }
1876
+ const seconds2 = options.durationSeconds ?? await probeDuration(source, signal);
1877
+ if (seconds2 < MIN_DURATION_SECONDS || Math.ceil(seconds2) > MAX_DURATION_SECONDS) {
1878
+ throw new FileLimitError({
1879
+ code: "duration_out_of_range",
1880
+ message: `The duration is ${String(seconds2)} s; it must be 15 s to 10 hours.`
1881
+ });
1882
+ }
1883
+ checkOptions(options);
1884
+ const declared = Math.ceil(seconds2);
1885
+ const data = await readAccount(api, signal);
1886
+ const { used_today_seconds: used, max_daily_seconds: max } = data.limits;
1887
+ if (used + declared > max) {
1888
+ throw new DailyLimitError({
1889
+ code: "daily_limit",
1890
+ message: `This upload would pass the daily limit (${String(used)} of ${String(max)} s used).`,
1891
+ details: { usedSeconds: used, maxSeconds: max },
1892
+ hintValues: hintValuesFrom({ usedSeconds: used, maxSeconds: max })
1893
+ });
1894
+ }
1895
+ return declared;
1896
+ }
1897
+
1898
+ // src/resources/agent.ts
1899
+ var CLAIM_ROUTE = "POST /v1/agent/accounts/{signup_id}/claim";
1900
+ var ROTATE_ROUTE = "POST /v1/agent/credentials/rotate";
1901
+ function lostCredential(route, status, received, cause) {
1902
+ return new OutcomeUnknownError({
1903
+ message: `${route} answered ${String(status)} without a usable token, so the token may be lost.`,
1904
+ hint: OutcomeUnknownError.hintFor(route),
1905
+ cause: badResponse(route, status, cause),
1906
+ ...received === void 0 ? {} : { receivedToken: received }
1907
+ });
1908
+ }
1909
+ function tokenOf(body, route, status, keepReceived) {
1910
+ const pat = body?.["pat"];
1911
+ if (typeof pat !== "string") throw lostCredential(route, status);
1912
+ registerSecret(pat.trim());
1913
+ try {
1914
+ return checkToken(pat);
1915
+ } catch (error) {
1916
+ const received = keepReceived && pat.trim() !== "" ? pat : void 0;
1917
+ throw lostCredential(route, status, received, error);
1918
+ }
1919
+ }
1920
+ var SIGNUP_FIELDS = /* @__PURE__ */ new Set([
1921
+ ...[
1922
+ "signup_id",
1923
+ "claim_secret",
1924
+ "checkout_url",
1925
+ "pack",
1926
+ "amount",
1927
+ "currency",
1928
+ "expires_at"
1929
+ ]
1930
+ ]);
1931
+ var CLAIM_FIELDS = /* @__PURE__ */ new Set(["pat", "balance_seconds", "user_id", "scopes"]);
1932
+ var ROTATE_FIELDS = /* @__PURE__ */ new Set(["pat", "pat_id", "scopes", "expires_at"]);
1933
+ function strings(value) {
1934
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1935
+ }
1936
+ function tokenlessApi(baseUrl) {
1937
+ const config = resolveTokenlessConfig({ baseUrl });
1938
+ const api = createClient({ baseUrl: config.baseUrl, fetch: createTransport(config) });
1939
+ api.use({
1940
+ onRequest: ({ request }) => {
1941
+ request.headers.set("user-agent", config.userAgent);
1942
+ }
1943
+ });
1944
+ return api;
1945
+ }
1946
+ var agentStatic = {
1947
+ async signup(request) {
1948
+ const terms = request.acceptTerms;
1949
+ if (typeof terms !== "string" || terms === "") {
1950
+ throw new ValidationError({
1951
+ code: "accept_terms_required",
1952
+ message: "acceptTerms is required: pass the policy version you accept. It is never filled in for you."
1953
+ });
1954
+ }
1955
+ const api = tokenlessApi(request.baseUrl);
1956
+ const body = await readBody(
1957
+ api.POST("/v1/agent/accounts", {
1958
+ body: {
1959
+ contact_email: request.email,
1960
+ pack_id: request.pack,
1961
+ accept_terms_version: request.acceptTerms,
1962
+ agent: {
1963
+ name: request.agent.name,
1964
+ platform: request.agent.platform,
1965
+ ...request.agent.contactUrl === void 0 ? {} : { contact_url: request.agent.contactUrl }
1966
+ },
1967
+ ...request.dataRegion === void 0 ? {} : { data_region: request.dataRegion },
1968
+ ...request.dryRun === void 0 ? {} : { dry_run: request.dryRun }
1969
+ },
1970
+ parseAs: "text"
1971
+ }),
1972
+ "POST /v1/agent/accounts",
1973
+ AGENT_SIGNUP
1974
+ );
1975
+ return withRedactedField(
1976
+ "AgentSignup",
1977
+ {
1978
+ ...unknownFields(body, SIGNUP_FIELDS),
1979
+ signupId: body.signup_id,
1980
+ claimSecret: body.claim_secret,
1981
+ checkoutUrl: body.checkout_url,
1982
+ price: toMoney(body.amount, body.currency),
1983
+ expiresAt: new Date(body.expires_at * 1e3)
1984
+ },
1985
+ "claimSecret"
1986
+ );
1987
+ },
1988
+ async claim(signupId, claimSecret, options = {}) {
1989
+ checkPathId(signupId, "signupId");
1990
+ if (typeof claimSecret === "string") registerSecret(claimSecret);
1991
+ const api = tokenlessApi(options.baseUrl);
1992
+ const { status, body } = await readObject(
1993
+ api.POST("/v1/agent/accounts/{signup_id}/claim", {
1994
+ // The OpenAPI document leaves the {signup_id} path parameter out of this operation, so its
1995
+ // generated type has no `path`; openapi-fetch still fills the template from it.
1996
+ params: { path: { signup_id: signupId } },
1997
+ body: { claim_secret: claimSecret },
1998
+ parseAs: "text"
1999
+ }),
2000
+ CLAIM_ROUTE
2001
+ );
2002
+ const pat = tokenOf(body, CLAIM_ROUTE, status, false);
2003
+ const seconds2 = body?.["balance_seconds"];
2004
+ const balanceSeconds = typeof seconds2 === "number" && Number.isInteger(seconds2) ? seconds2 : 0;
2005
+ const userId = body?.["user_id"];
2006
+ return withRedactedField(
2007
+ "AgentClaim",
2008
+ {
2009
+ ...body === null ? {} : unknownFields(body, CLAIM_FIELDS),
2010
+ token: pat,
2011
+ userId: typeof userId === "string" ? userId : "",
2012
+ scopes: strings(body?.["scopes"]),
2013
+ // A fresh account has no holds; the claim answer carries no expiry, and a follow-up read
2014
+ // could fail after the token was shown for the only time.
2015
+ balance: {
2016
+ seconds: balanceSeconds,
2017
+ minutes: Math.trunc(balanceSeconds / 60),
2018
+ heldSeconds: 0,
2019
+ expiresAt: null,
2020
+ purchasesRemaining: null
2021
+ }
2022
+ },
2023
+ "token"
2024
+ );
2025
+ }
2026
+ };
2027
+ var AgentResource = class {
2028
+ #api;
2029
+ #config;
2030
+ constructor(api, config) {
2031
+ this.#api = api;
2032
+ this.#config = config;
2033
+ }
2034
+ /**
2035
+ * Mints a successor token and revokes the one in use. The client starts using the new token at
2036
+ * once; store the returned one. A lost answer raises `OutcomeUnknownError` and is never retried.
2037
+ */
2038
+ async rotateToken() {
2039
+ const { status, body } = await readObject(
2040
+ this.#api.POST("/v1/agent/credentials/rotate", { parseAs: "text" }),
2041
+ ROTATE_ROUTE
2042
+ );
2043
+ const pat = tokenOf(body, ROTATE_ROUTE, status, true);
2044
+ const expiresAt = body?.["expires_at"];
2045
+ if (expiresAt !== void 0 && expiresAt !== null) {
2046
+ const found = timestamp(expiresAt);
2047
+ if (found !== null) {
2048
+ const problem2 = describeProblem({ path: ["expires_at"], expected: found.expected });
2049
+ throw lostCredential(ROTATE_ROUTE, status, pat, new Error(problem2));
2050
+ }
2051
+ }
2052
+ this.#config.setToken(pat);
2053
+ const tokenId = body?.["pat_id"];
2054
+ return withRedactedField(
2055
+ "RotatedToken",
2056
+ {
2057
+ ...body === null ? {} : unknownFields(body, ROTATE_FIELDS),
2058
+ token: pat,
2059
+ tokenId: typeof tokenId === "string" ? tokenId : "",
2060
+ scopes: strings(body?.["scopes"]),
2061
+ expiresAt: typeof expiresAt === "number" ? new Date(expiresAt * 1e3) : null
2062
+ },
2063
+ "token"
2064
+ );
2065
+ }
2066
+ };
2067
+
2068
+ // src/paginate.ts
2069
+ function nextCursorOf(cursor) {
2070
+ return cursor === "" ? null : cursor;
2071
+ }
2072
+ function assertCursorAdvanced(sent, next) {
2073
+ if (next !== null && next === sent) {
2074
+ throw new HushscriptError({
2075
+ code: "pagination_loop",
2076
+ message: "The API returned the same page cursor it was sent; stopping instead of repeating the page."
2077
+ });
2078
+ }
2079
+ }
2080
+ function stopIfAborted(signal) {
2081
+ if (signal?.aborted === true) throw aborted(signal);
2082
+ }
2083
+ async function* paginate(fetchPage, signal) {
2084
+ let cursor = null;
2085
+ do {
2086
+ stopIfAborted(signal);
2087
+ const sent = cursor;
2088
+ const page = await fetchPage(sent);
2089
+ for (const item of page.items) {
2090
+ stopIfAborted(signal);
2091
+ yield item;
2092
+ }
2093
+ cursor = nextCursorOf(page.nextCursor);
2094
+ assertCursorAdvanced(sent, cursor);
2095
+ } while (cursor !== null);
2096
+ }
2097
+
2098
+ // src/resources/billing.ts
2099
+ var PURCHASE_KEY = /^[\w-]{1,128}$/;
2100
+ var PURCHASE_ROUTE = "POST /v1/billing/purchase";
2101
+ function date(unixSeconds) {
2102
+ return new Date(unixSeconds * 1e3);
2103
+ }
2104
+ var PACK_FIELDS = /* @__PURE__ */ new Set([
2105
+ ...[
2106
+ "id",
2107
+ "amount",
2108
+ "currency",
2109
+ "seconds",
2110
+ "referral_bonus_seconds",
2111
+ "available"
2112
+ ]
2113
+ ]);
2114
+ var CARD_FIELDS = /* @__PURE__ */ new Set([
2115
+ ...[
2116
+ "id",
2117
+ "type",
2118
+ "brand",
2119
+ "last4",
2120
+ "exp_month",
2121
+ "exp_year",
2122
+ "email",
2123
+ "is_default",
2124
+ "fast_checkout_eligible"
2125
+ ]
2126
+ ]);
2127
+ var TRANSACTION_FIELDS = /* @__PURE__ */ new Set([
2128
+ ...[
2129
+ "id",
2130
+ "seconds",
2131
+ "amount_cents",
2132
+ "amount_refunded_cents",
2133
+ "currency",
2134
+ "status",
2135
+ "created_at",
2136
+ "payment_method_type",
2137
+ "payment_method_brand",
2138
+ "payment_method_last4",
2139
+ "receipt_number",
2140
+ "initiated_via"
2141
+ ]
2142
+ ]);
2143
+ var LEDGER_FIELDS = /* @__PURE__ */ new Set([
2144
+ ...[
2145
+ "id",
2146
+ "delta_seconds",
2147
+ "kind",
2148
+ "job_id",
2149
+ "note",
2150
+ "created_at"
2151
+ ]
2152
+ ]);
2153
+ var HOLD_FIELDS = /* @__PURE__ */ new Set([
2154
+ ...[
2155
+ "id",
2156
+ "kind",
2157
+ "seconds",
2158
+ "created_at"
2159
+ ]
2160
+ ]);
2161
+ var CARDS_FIELDS = /* @__PURE__ */ new Set(["default_id", "methods"]);
2162
+ var PURCHASE_FIELDS = /* @__PURE__ */ new Set(["status", "balance_seconds"]);
2163
+ function packOf(raw) {
2164
+ return {
2165
+ ...unknownFields(raw, PACK_FIELDS),
2166
+ id: raw.id,
2167
+ seconds: raw.seconds,
2168
+ minutes: Math.trunc(raw.seconds / 60),
2169
+ price: toMoney(raw.amount, raw.currency),
2170
+ bonusMinutes: Math.trunc(raw.referral_bonus_seconds / 60),
2171
+ available: raw.available ?? true
2172
+ };
2173
+ }
2174
+ function chosenCard(saved) {
2175
+ if (saved.defaultId !== null) return saved.defaultId;
2176
+ const eligible = saved.cards.filter((card) => card.fastCheckoutEligible);
2177
+ const [only] = eligible;
2178
+ if (only === void 0) {
2179
+ throw new PaymentMethodRequiredError({
2180
+ message: "The account has no saved card that can be charged."
2181
+ });
2182
+ }
2183
+ if (eligible.length > 1) {
2184
+ throw new PaymentMethodRequiredError({
2185
+ code: "default_card_required",
2186
+ message: "The account has no default card and several that can be charged."
2187
+ });
2188
+ }
2189
+ return only.id;
2190
+ }
2191
+ var BillingResource = class {
2192
+ #api;
2193
+ constructor(api) {
2194
+ this.#api = api;
2195
+ }
2196
+ /** The minute packs this account can buy, priced in its own currency. Needs the `billing` scope. */
2197
+ async packs() {
2198
+ const body = await readBody(
2199
+ this.#api.GET("/v1/billing/packs", { parseAs: "text" }),
2200
+ "GET /v1/billing/packs",
2201
+ PACKS
2202
+ );
2203
+ return body.packs.map(packOf);
2204
+ }
2205
+ async cards() {
2206
+ const body = await readBody(
2207
+ this.#api.GET("/v1/billing/cards", { parseAs: "text" }),
2208
+ "GET /v1/billing/cards",
2209
+ CARDS
2210
+ );
2211
+ return {
2212
+ ...unknownFields(body, CARDS_FIELDS),
2213
+ defaultId: body.default_id,
2214
+ cards: body.methods.map((m) => camelizeKnown(m, CARD_FIELDS))
2215
+ };
2216
+ }
2217
+ async setDefaultCard(cardId) {
2218
+ checkPathId(cardId, "cardId");
2219
+ await readBody(
2220
+ this.#api.POST("/v1/billing/cards/default", {
2221
+ body: { payment_method_id: cardId },
2222
+ parseAs: "text"
2223
+ }),
2224
+ "POST /v1/billing/cards/default",
2225
+ SET_DEFAULT_CARD
2226
+ );
2227
+ }
2228
+ /**
2229
+ * The hosted page where a card is saved; open it in a browser. The request carries a key, so
2230
+ * the single retry after a 502 lands on the same page.
2231
+ */
2232
+ async addCardUrl() {
2233
+ const body = await readBody(
2234
+ this.#api.POST("/v1/billing/setup-session", {
2235
+ body: { idempotency_key: randomUUID() },
2236
+ parseAs: "text"
2237
+ }),
2238
+ "POST /v1/billing/setup-session",
2239
+ SETUP_SESSION
2240
+ );
2241
+ return body.checkout_url;
2242
+ }
2243
+ /**
2244
+ * Buys `packId` with the default card and returns the balance the purchase answer reports. It
2245
+ * makes no follow-up request. A lost or unusable response can still raise an error after payment.
2246
+ * Every error raised once the purchase request may have been sent carries `idempotencyKey`; pass it back as
2247
+ * `idempotencyKey` to retry without any chance of a second charge. One raised before any attempt
2248
+ * was sent carries none: nothing can have been charged.
2249
+ */
2250
+ async buy(packId, options = {}) {
2251
+ const key = options.idempotencyKey === void 0 ? randomUUID() : options.idempotencyKey;
2252
+ if (typeof key !== "string" || !PURCHASE_KEY.test(key)) {
2253
+ throw invalidOption(
2254
+ "idempotencyKey must be 1 to 128 characters from A-Z, a-z, 0-9, _ and -."
2255
+ );
2256
+ }
2257
+ const card = chosenCard(await this.cards());
2258
+ try {
2259
+ const { status, body } = await readObject(
2260
+ this.#api.POST("/v1/billing/purchase", {
2261
+ body: {
2262
+ pack: packId,
2263
+ idempotency_key: key,
2264
+ quick_payment_method_id: card,
2265
+ policy_ack: options.acknowledgeNoRefund === true
2266
+ },
2267
+ parseAs: "text"
2268
+ }),
2269
+ PURCHASE_ROUTE
2270
+ );
2271
+ const seconds2 = body?.["balance_seconds"];
2272
+ if (body?.["status"] !== "ok" || typeof seconds2 !== "number" || !Number.isSafeInteger(seconds2)) {
2273
+ throw badResponse(PURCHASE_ROUTE, status);
2274
+ }
2275
+ return {
2276
+ ...unknownFields(body, PURCHASE_FIELDS),
2277
+ seconds: seconds2,
2278
+ minutes: Math.trunc(seconds2 / 60) + 0
2279
+ };
2280
+ } catch (error) {
2281
+ const failure = asHushscriptError(error);
2282
+ if (!wasNotSent(failure)) failure.idempotencyKey = key;
2283
+ throw failure;
2284
+ }
2285
+ }
2286
+ /** Purchases, newest first. */
2287
+ transactions(options = {}) {
2288
+ return paginate(async (cursor) => {
2289
+ const page = await readBody(
2290
+ this.#api.GET("/v1/billing/transactions", {
2291
+ params: { query: cursor === null ? {} : { cursor } },
2292
+ parseAs: "text",
2293
+ ...options.signal === void 0 ? {} : { signal: options.signal }
2294
+ }),
2295
+ "GET /v1/billing/transactions",
2296
+ TRANSACTIONS
2297
+ );
2298
+ return {
2299
+ items: page.transactions.map(
2300
+ (row) => ({
2301
+ ...camelizeKnown(row, TRANSACTION_FIELDS),
2302
+ createdAt: date(row.created_at)
2303
+ })
2304
+ ),
2305
+ nextCursor: nextCursorOf(page.next_cursor)
2306
+ };
2307
+ }, options.signal);
2308
+ }
2309
+ /** Every hold, settle, release, purchase and refund that moved the balance, newest first. */
2310
+ ledger(options = {}) {
2311
+ return paginate(async (cursor) => {
2312
+ const page = await readBody(
2313
+ this.#api.GET("/v1/billing/ledger", {
2314
+ params: { query: cursor === null ? {} : { cursor } },
2315
+ parseAs: "text",
2316
+ ...options.signal === void 0 ? {} : { signal: options.signal }
2317
+ }),
2318
+ "GET /v1/billing/ledger",
2319
+ LEDGER
2320
+ );
2321
+ return {
2322
+ items: page.entries.map(
2323
+ (row) => ({
2324
+ ...camelizeKnown(row, LEDGER_FIELDS),
2325
+ createdAt: date(row.created_at)
2326
+ })
2327
+ ),
2328
+ nextCursor: nextCursorOf(page.next_cursor)
2329
+ };
2330
+ }, options.signal);
2331
+ }
2332
+ /** The seconds currently reserved by work in flight, newest first. */
2333
+ holds(options = {}) {
2334
+ return paginate(async (cursor) => {
2335
+ const page = await readBody(
2336
+ this.#api.GET("/v1/billing/holds", {
2337
+ params: { query: cursor === null ? {} : { cursor } },
2338
+ parseAs: "text",
2339
+ ...options.signal === void 0 ? {} : { signal: options.signal }
2340
+ }),
2341
+ "GET /v1/billing/holds",
2342
+ HOLDS
2343
+ );
2344
+ return {
2345
+ items: page.holds.map(
2346
+ (row) => ({
2347
+ ...camelizeKnown(row, HOLD_FIELDS),
2348
+ createdAt: date(row.created_at)
2349
+ })
2350
+ ),
2351
+ nextCursor: nextCursorOf(page.next_cursor)
2352
+ };
2353
+ }, options.signal);
2354
+ }
2355
+ };
2356
+
2357
+ // src/transcribe/wait.ts
2358
+ var DEFAULT_WAIT_TIMEOUT_SECONDS = 3600;
2359
+ var POLL_START_MS = 5e3;
2360
+ var POLL_FACTOR = 1.5;
2361
+ var POLL_CEILING_MS = 15e3;
2362
+ function isFinished(job) {
2363
+ return job.state === "done" || job.state === "failed";
2364
+ }
2365
+ function failureOf(job) {
2366
+ const kind = job.failure_kind === "media_unreadable" || job.failure_kind === "media_undecodable";
2367
+ return new JobFailedError({
2368
+ message: job.error ?? "The job failed.",
2369
+ error: job.error,
2370
+ failureKind: kind ? job.failure_kind : null,
2371
+ jobId: job.id
2372
+ });
2373
+ }
2374
+ async function readJob(api, id, signal) {
2375
+ return readBody(
2376
+ api.GET("/v1/jobs/{id}", {
2377
+ params: { path: { id } },
2378
+ parseAs: "text",
2379
+ signal: signal ?? null
2380
+ }),
2381
+ "GET /v1/jobs/{id}",
2382
+ JOB_ONE
2383
+ );
2384
+ }
2385
+ async function waitForJob(settings) {
2386
+ const { api, clock, jobId, signal } = settings;
2387
+ const startedAt = clock.now();
2388
+ const deadline = startedAt + settings.waitTimeout * 1e3;
2389
+ let interval = POLL_START_MS;
2390
+ for (; ; ) {
2391
+ const job = await readJob(api, jobId, signal);
2392
+ if (isFinished(job)) return job;
2393
+ if (job.state === "queued" || job.state === "processing") {
2394
+ settings.onProgress?.({ stage: "processing", state: job.state });
2395
+ }
2396
+ const remaining = deadline - clock.now();
2397
+ if (remaining <= 0) {
2398
+ throw new WaitTimeoutError({
2399
+ message: `The job is still ${job.state} after ${String(settings.waitTimeout)} s; it keeps running.`,
2400
+ resumeToken: settings.resumeToken
2401
+ });
2402
+ }
2403
+ await sleepOrAbort(clock, Math.min(interval, remaining), signal);
2404
+ interval = Math.min(interval * POLL_FACTOR, POLL_CEILING_MS);
2405
+ }
2406
+ }
2407
+
2408
+ // src/resources/jobs.ts
2409
+ var JOB_FIELDS = /* @__PURE__ */ new Set([
2410
+ ...[
2411
+ "id",
2412
+ "state",
2413
+ "mode",
2414
+ "declared_seconds",
2415
+ "verified_seconds",
2416
+ "actual_seconds",
2417
+ "billable_seconds",
2418
+ "error",
2419
+ "failure_kind",
2420
+ "retry_of",
2421
+ "created_at",
2422
+ "transcript_id",
2423
+ "private_result_expires_at",
2424
+ "private_result_available",
2425
+ "options",
2426
+ "fetched_bytes",
2427
+ "fetch_total_bytes"
2428
+ ]
2429
+ ]);
2430
+ function toJob(raw) {
2431
+ return {
2432
+ ...unknownFields(raw, JOB_FIELDS),
2433
+ id: raw.id,
2434
+ state: raw.state,
2435
+ transcriptId: raw.transcript_id,
2436
+ error: raw.error,
2437
+ failureKind: raw.failure_kind,
2438
+ mode: raw.mode,
2439
+ declaredSeconds: raw.declared_seconds,
2440
+ verifiedSeconds: raw.verified_seconds,
2441
+ actualSeconds: raw.actual_seconds,
2442
+ billableSeconds: raw.billable_seconds,
2443
+ retryOf: raw.retry_of,
2444
+ createdAt: new Date(raw.created_at * 1e3),
2445
+ options: camelize(raw.options)
2446
+ };
2447
+ }
2448
+ var JobsResource = class {
2449
+ #api;
2450
+ #wait;
2451
+ constructor(api, wait) {
2452
+ this.#api = api;
2453
+ this.#wait = wait;
2454
+ }
2455
+ /** Every job, newest first; `active` keeps only the ones that have not finished. `signal` stops the walk between and during pages. */
2456
+ list(options = {}) {
2457
+ const { signal } = options;
2458
+ return paginate(async (cursor) => {
2459
+ const query2 = {};
2460
+ if (options.active === true) query2.active = "true";
2461
+ if (cursor !== null) query2.cursor = cursor;
2462
+ const page = await readBody(
2463
+ this.#api.GET("/v1/jobs", {
2464
+ params: { query: query2 },
2465
+ parseAs: "text",
2466
+ ...signal === void 0 ? {} : { signal }
2467
+ }),
2468
+ "GET /v1/jobs",
2469
+ JOB_LIST
2470
+ );
2471
+ return { items: page.jobs.map(toJob), nextCursor: nextCursorOf(page.next_cursor) };
2472
+ }, signal);
2473
+ }
2474
+ async get(id) {
2475
+ return toJob(await readJob(this.#api, checkPathId(id, "id")));
2476
+ }
2477
+ /** Polls until the job is done or failed; times out with a `WaitTimeoutError` that has no resume token. */
2478
+ async wait(id, options) {
2479
+ return toJob(await this.#wait(checkPathId(id, "id"), options));
2480
+ }
2481
+ };
2482
+
2483
+ // src/format/captions.ts
2484
+ var MAX_CHARS_PER_LINE = 42;
2485
+ var MAX_LINES = 2;
2486
+ var MAX_CUE_MS = 7e3;
2487
+ function wrapIntoCues(text, maxChars, maxLines) {
2488
+ const words = text.split(/\s+/).filter(Boolean);
2489
+ if (words.length === 0) return [""];
2490
+ const cues = [];
2491
+ let lines = [];
2492
+ let line = "";
2493
+ const flushLine = () => {
2494
+ if (line !== "") lines.push(line);
2495
+ line = "";
2496
+ };
2497
+ const flushCue = () => {
2498
+ flushLine();
2499
+ if (lines.length > 0) cues.push(lines.join("\n"));
2500
+ lines = [];
2501
+ };
2502
+ for (const word of words) {
2503
+ const candidate = line === "" ? word : `${line} ${word}`;
2504
+ if (candidate.length <= maxChars) {
2505
+ line = candidate;
2506
+ } else {
2507
+ flushLine();
2508
+ if (lines.length >= maxLines) flushCue();
2509
+ line = word;
2510
+ }
2511
+ }
2512
+ flushCue();
2513
+ return cues.length > 0 ? cues : [""];
2514
+ }
2515
+ function utteranceCues(utterances) {
2516
+ return utterances.map((u) => ({
2517
+ startMs: u.startMs,
2518
+ endMs: u.endMs,
2519
+ speaker: u.speaker,
2520
+ speakerName: u.speakerName,
2521
+ text: u.text
2522
+ }));
2523
+ }
2524
+ function segmentCaptions(utterances) {
2525
+ const textChars = (cue) => cue.replaceAll("\n", " ").length;
2526
+ const out = [];
2527
+ for (const u of utterances) {
2528
+ const chunks = wrapIntoCues(u.text, MAX_CHARS_PER_LINE, MAX_LINES);
2529
+ const span = Math.max(0, u.endMs - u.startMs);
2530
+ const totalChars = chunks.reduce((sum, chunk) => sum + textChars(chunk), 0) || 1;
2531
+ let accChars = 0;
2532
+ chunks.forEach((text, index) => {
2533
+ const startMs = u.startMs + Math.round(span * accChars / totalChars);
2534
+ accChars += Math.max(1, textChars(text));
2535
+ let endMs = index === chunks.length - 1 ? u.endMs : u.startMs + Math.round(span * accChars / totalChars);
2536
+ if (endMs - startMs > MAX_CUE_MS) endMs = startMs + MAX_CUE_MS;
2537
+ if (endMs < startMs) endMs = startMs;
2538
+ out.push({ startMs, endMs, speaker: u.speaker, speakerName: u.speakerName, text });
2539
+ });
2540
+ }
2541
+ return out;
2542
+ }
2543
+
2544
+ // src/format/time.ts
2545
+ function pad(n, width = 2) {
2546
+ return String(n).padStart(width, "0");
2547
+ }
2548
+ function secondsToClock(totalSeconds) {
2549
+ const s = Math.max(0, Math.floor(totalSeconds));
2550
+ const h = Math.floor(s / 3600);
2551
+ const m = Math.floor(s % 3600 / 60);
2552
+ const sec = s % 60;
2553
+ return h > 0 ? `${String(h)}:${pad(m)}:${pad(sec)}` : `${String(m)}:${pad(sec)}`;
2554
+ }
2555
+ function msToClock(ms) {
2556
+ return secondsToClock(Math.floor(ms / 1e3));
2557
+ }
2558
+ function srtTimestamp(ms, separator = ",") {
2559
+ const total = Math.max(0, Math.floor(ms));
2560
+ const h = Math.floor(total / 36e5);
2561
+ const m = Math.floor(total % 36e5 / 6e4);
2562
+ const s = Math.floor(total % 6e4 / 1e3);
2563
+ return `${pad(h)}:${pad(m)}:${pad(s)}${separator}${pad(total % 1e3, 3)}`;
2564
+ }
2565
+
2566
+ // src/format/srt.ts
2567
+ function cueText(text) {
2568
+ return text.split(/\r\n?|\n/).filter((line) => line.trim() !== "").join("\n");
2569
+ }
2570
+ function cuesFor(utterances, options) {
2571
+ return options.captions === false ? utteranceCues(utterances) : segmentCaptions(utterances);
2572
+ }
2573
+ function withSpeaker(cue) {
2574
+ return cue.speakerName === "" ? cue.text : `${cue.speakerName}: ${cue.text}`;
2575
+ }
2576
+ function arrowSafeCueText(text) {
2577
+ return cueText(text).replace(/-{2,}>/g, "->");
2578
+ }
2579
+ function renderSrt(utterances, options) {
2580
+ return cuesFor(utterances, options).map(
2581
+ (cue, index) => `${String(index + 1)}
2582
+ ${srtTimestamp(cue.startMs)} --> ${srtTimestamp(cue.endMs)}
2583
+ ${arrowSafeCueText(withSpeaker(cue))}`
2584
+ ).join("\n\n");
2585
+ }
2586
+
2587
+ // src/format/txt.ts
2588
+ function blocksOf(utterances, merge) {
2589
+ const blocks = [];
2590
+ for (const u of utterances) {
2591
+ const last = blocks.at(-1);
2592
+ if (merge && last?.speaker === u.speaker) {
2593
+ last.text += ` ${u.text}`;
2594
+ } else {
2595
+ blocks.push({
2596
+ speaker: u.speaker,
2597
+ speakerName: u.speakerName,
2598
+ startMs: u.startMs,
2599
+ text: u.text
2600
+ });
2601
+ }
2602
+ }
2603
+ return blocks;
2604
+ }
2605
+ function renderText(utterances, options) {
2606
+ return blocksOf(utterances, options.merge !== false).map((block) => {
2607
+ const who = options.speakers === false ? "" : block.speakerName;
2608
+ const prefix = options.timestamps === true ? `[${msToClock(block.startMs)}] ` : "";
2609
+ return `${prefix}${who === "" ? block.text : `${who}: ${block.text}`}`;
2610
+ }).join("\n\n");
2611
+ }
2612
+
2613
+ // src/format/vtt.ts
2614
+ function vttCueText(text) {
2615
+ return cueText(text).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll("-->", "--&gt;");
2616
+ }
2617
+ function renderVtt(utterances, options) {
2618
+ const body = cuesFor(utterances, options).map(
2619
+ (cue, index) => `${String(index + 1)}
2620
+ ${srtTimestamp(cue.startMs, ".")} --> ${srtTimestamp(cue.endMs, ".")}
2621
+ ${vttCueText(withSpeaker(cue))}`
2622
+ ).join("\n\n");
2623
+ return `WEBVTT
2624
+
2625
+ ${body}
2626
+ `;
2627
+ }
2628
+
2629
+ // src/transcript.ts
2630
+ var CLEANED_KEY = "cleaned";
2631
+ function seconds(value) {
2632
+ return new Date(value * 1e3);
2633
+ }
2634
+ function displayName(label, names) {
2635
+ const saved = Object.hasOwn(names, label) ? names[label] : void 0;
2636
+ const named = typeof saved === "string" ? saved.trim() : "";
2637
+ if (named !== "") return named;
2638
+ return label.trim() === "" ? "" : `Speaker ${label}`;
2639
+ }
2640
+ function utterancesOf(language, names) {
2641
+ return language.utterances.map((u) => ({
2642
+ // Fields the API adds later stay on the utterance, camelCased; the known ones below win.
2643
+ ...camelize(u),
2644
+ speaker: u.speaker,
2645
+ speakerName: displayName(u.speaker, names),
2646
+ startMs: u.start_ms,
2647
+ endMs: u.end_ms,
2648
+ text: u.text
2649
+ }));
2650
+ }
2651
+ var Transcript = class _Transcript {
2652
+ id;
2653
+ title;
2654
+ language;
2655
+ durationSeconds;
2656
+ createdAt;
2657
+ updatedAt;
2658
+ autoDeleteAt;
2659
+ tags;
2660
+ edited;
2661
+ /** Label to display name, for the speakers that were renamed. */
2662
+ speakers;
2663
+ /** The original language. */
2664
+ utterances;
2665
+ /** Translation locales, without the reserved "cleaned". */
2666
+ languages;
2667
+ /** The AI-cleaned body, when there is one. */
2668
+ cleaned;
2669
+ #raw;
2670
+ #translations = /* @__PURE__ */ new Map();
2671
+ constructor(raw) {
2672
+ this.#raw = raw;
2673
+ this.id = raw.id;
2674
+ this.title = raw.title;
2675
+ this.language = raw.language_detected;
2676
+ this.durationSeconds = raw.duration_seconds;
2677
+ this.createdAt = seconds(raw.created_at);
2678
+ this.updatedAt = seconds(raw.updated_at);
2679
+ this.autoDeleteAt = raw.auto_delete_at === null ? null : seconds(raw.auto_delete_at);
2680
+ this.tags = raw.tags;
2681
+ this.edited = raw.edited;
2682
+ this.speakers = raw.speaker_names;
2683
+ this.utterances = utterancesOf(raw.body.original, raw.speaker_names);
2684
+ const translations = raw.body.translations;
2685
+ this.languages = Object.keys(translations).filter((key) => key !== CLEANED_KEY);
2686
+ this.cleaned = Object.hasOwn(translations, CLEANED_KEY) ? this.#translation(CLEANED_KEY) : null;
2687
+ }
2688
+ /** Builds a transcript from a `GET /v1/transcripts/{id}` body, which `toJSON()` returns again. */
2689
+ static fromApi(body) {
2690
+ const found = TRANSCRIPT_DETAIL(body);
2691
+ if (found !== null) {
2692
+ throw badResponse("GET /v1/transcripts/{id}", null, new Error(describeProblem(found)));
2693
+ }
2694
+ return new _Transcript(body);
2695
+ }
2696
+ #translation(language) {
2697
+ const known = this.#translations.get(language);
2698
+ if (known !== void 0) return known;
2699
+ const body = this.#raw.body.translations[language];
2700
+ const built = body === void 0 ? this.utterances : utterancesOf(body, this.speakers);
2701
+ this.#translations.set(language, built);
2702
+ return built;
2703
+ }
2704
+ /** The plain text, the same as `toText()`. */
2705
+ get text() {
2706
+ return this.toText();
2707
+ }
2708
+ /** The translation (or "cleaned" body) for `language`; the original when there is none. */
2709
+ utterancesFor(language) {
2710
+ if (language === void 0 || language === "original") return this.utterances;
2711
+ return Object.hasOwn(this.#raw.body.translations, language) ? this.#translation(language) : this.utterances;
2712
+ }
2713
+ toSRT(options = {}) {
2714
+ return renderSrt(this.utterancesFor(options.language), options);
2715
+ }
2716
+ toVTT(options = {}) {
2717
+ return renderVtt(this.utterancesFor(options.language), options);
2718
+ }
2719
+ toText(options = {}) {
2720
+ return renderText(this.utterancesFor(options.language), options);
2721
+ }
2722
+ /** The API body this transcript was built from, unknown fields included. */
2723
+ toJSON() {
2724
+ return this.#raw;
2725
+ }
2726
+ };
2727
+
2728
+ // src/resources/transcripts.ts
2729
+ function date2(unixSeconds) {
2730
+ return new Date(unixSeconds * 1e3);
2731
+ }
2732
+ function signalOf(signal) {
2733
+ return signal === void 0 ? {} : { signal };
2734
+ }
2735
+ function query(options) {
2736
+ const out = {};
2737
+ if (options.pageSize !== void 0) {
2738
+ if (!Number.isInteger(options.pageSize) || options.pageSize < 1) {
2739
+ throw invalidOption("pageSize must be a whole number of 1 or more.");
2740
+ }
2741
+ out.limit = options.pageSize;
2742
+ }
2743
+ if (options.cursor !== void 0) out.cursor = options.cursor;
2744
+ return out;
2745
+ }
2746
+ var SUMMARY_FIELDS = /* @__PURE__ */ new Set([
2747
+ ...[
2748
+ "id",
2749
+ "title",
2750
+ "language_detected",
2751
+ "duration_seconds",
2752
+ "created_at",
2753
+ "auto_delete_at",
2754
+ "tags",
2755
+ "options",
2756
+ "insights_state"
2757
+ ]
2758
+ ]);
2759
+ var DELETED_FIELDS = /* @__PURE__ */ new Set([
2760
+ ...[
2761
+ "id",
2762
+ "title",
2763
+ "duration_seconds",
2764
+ "created_at",
2765
+ "deleted_at",
2766
+ "cause",
2767
+ "receipt"
2768
+ ]
2769
+ ]);
2770
+ function summaryOf(row) {
2771
+ return {
2772
+ ...unknownFields(row, SUMMARY_FIELDS),
2773
+ id: row.id,
2774
+ title: row.title,
2775
+ language: row.language_detected,
2776
+ durationSeconds: row.duration_seconds,
2777
+ createdAt: date2(row.created_at),
2778
+ autoDeleteAt: row.auto_delete_at === null ? null : date2(row.auto_delete_at),
2779
+ tags: row.tags
2780
+ };
2781
+ }
2782
+ function deletedOf(row) {
2783
+ return {
2784
+ ...unknownFields(row, DELETED_FIELDS),
2785
+ id: row.id,
2786
+ title: row.title,
2787
+ durationSeconds: row.duration_seconds,
2788
+ createdAt: date2(row.created_at),
2789
+ deletedAt: date2(row.deleted_at),
2790
+ cause: row.cause,
2791
+ receipt: row.receipt
2792
+ };
2793
+ }
2794
+ function readTranscript(api, id, signal) {
2795
+ return readBody(
2796
+ api.GET("/v1/transcripts/{id}", {
2797
+ params: { path: { id } },
2798
+ parseAs: "text",
2799
+ ...signalOf(signal)
2800
+ }),
2801
+ "GET /v1/transcripts/{id}",
2802
+ TRANSCRIPT_DETAIL
2803
+ );
2804
+ }
2805
+ var TranscriptsResource = class {
2806
+ #api;
2807
+ constructor(api) {
2808
+ this.#api = api;
2809
+ }
2810
+ /**
2811
+ * Every finished transcript, newest first, fetching pages as it goes. Jobs still running are in `jobs.list({ active: true })`.
2812
+ * A page whose `next_cursor` repeats the cursor sent is yielded whole, then the walk raises `pagination_loop`.
2813
+ */
2814
+ list(options = {}) {
2815
+ query(options);
2816
+ return paginate(
2817
+ (cursor) => this.#fetchPage({ ...options, ...cursor === null ? {} : { cursor } }),
2818
+ options.signal
2819
+ );
2820
+ }
2821
+ /** One page of `list()`. Raises `pagination_loop` when the answer's `next_cursor` is the cursor sent. */
2822
+ async page(options = {}) {
2823
+ const page = await this.#fetchPage(options);
2824
+ assertCursorAdvanced(options.cursor ?? null, page.nextCursor);
2825
+ return page;
2826
+ }
2827
+ async #fetchPage(options) {
2828
+ const body = await readBody(
2829
+ this.#api.GET("/v1/transcripts", {
2830
+ params: { query: query(options) },
2831
+ parseAs: "text",
2832
+ ...signalOf(options.signal)
2833
+ }),
2834
+ "GET /v1/transcripts",
2835
+ TRANSCRIPT_LIST
2836
+ );
2837
+ return { items: body.transcripts.map(summaryOf), nextCursor: nextCursorOf(body.next_cursor) };
2838
+ }
2839
+ async get(id) {
2840
+ return Transcript.fromApi(await readTranscript(this.#api, checkPathId(id, "id")));
2841
+ }
2842
+ /** Deletion records, by upload time, newest first. */
2843
+ deleted(options = {}) {
2844
+ return paginate(async (cursor) => {
2845
+ const body = await readBody(
2846
+ this.#api.GET("/v1/transcripts/deleted", {
2847
+ params: { query: cursor === null ? {} : { cursor } },
2848
+ parseAs: "text",
2849
+ ...signalOf(options.signal)
2850
+ }),
2851
+ "GET /v1/transcripts/deleted",
2852
+ DELETED_LIST
2853
+ );
2854
+ return { items: body.deleted.map(deletedOf), nextCursor: nextCursorOf(body.next_cursor) };
2855
+ }, options.signal);
2856
+ }
2857
+ };
2858
+
2859
+ // src/transcribe/heartbeat.ts
2860
+ var HEARTBEAT_INTERVAL_MS = 12e4;
2861
+ async function runHeartbeat(options) {
2862
+ const { clock, signal, send } = options;
2863
+ const interval = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
2864
+ let nextAt = clock.now() + interval;
2865
+ for (; ; ) {
2866
+ try {
2867
+ await clock.sleep(Math.max(0, nextAt - clock.now()), signal);
2868
+ } catch {
2869
+ return;
2870
+ }
2871
+ nextAt = Math.max(nextAt, clock.now()) + interval;
2872
+ try {
2873
+ await send();
2874
+ } catch (error) {
2875
+ if (signal.aborted) return;
2876
+ if (error instanceof AuthenticationError) throw error;
2877
+ if (error instanceof HushscriptError && error.code === "wrong_state") return;
2878
+ }
2879
+ }
2880
+ }
2881
+
2882
+ // src/transcribe/resume.ts
2883
+ function encodeResumeToken(state) {
2884
+ const body = {
2885
+ v: 1,
2886
+ jobId: state.jobId,
2887
+ idempotencyKey: state.idempotencyKey,
2888
+ size: state.size,
2889
+ durationSeconds: state.durationSeconds
2890
+ };
2891
+ return Buffer.from(JSON.stringify(body)).toString("base64url");
2892
+ }
2893
+ var isWhole = (value) => Number.isSafeInteger(value);
2894
+ var isFiniteNumber = (value) => Number.isFinite(value);
2895
+ function invalid() {
2896
+ return new ConfigurationError({ message: "The resume token is not valid." });
2897
+ }
2898
+ function decodeResumeToken(token) {
2899
+ let parsed;
2900
+ try {
2901
+ parsed = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
2902
+ } catch {
2903
+ throw invalid();
2904
+ }
2905
+ if (parsed === null) throw invalid();
2906
+ const body = parsed;
2907
+ const { jobId, idempotencyKey, size, durationSeconds } = body;
2908
+ if (body["v"] !== 1 || !isPathSegment(jobId) || typeof idempotencyKey !== "string" || idempotencyKey === "" || !isWhole(size) || size < 1 || !isFiniteNumber(durationSeconds) || durationSeconds <= 0) {
2909
+ throw invalid();
2910
+ }
2911
+ return { jobId, idempotencyKey, size, durationSeconds };
2912
+ }
2913
+ function assertSameFile(state, size) {
2914
+ if (size === state.size) return;
2915
+ throw new ResumeMismatchError({
2916
+ code: "resume_file_mismatch",
2917
+ message: `The file is ${String(size)} bytes; the upload started with ${String(state.size)} bytes.`
2918
+ });
2919
+ }
2920
+ function fileRequired() {
2921
+ return new ResumeMismatchError({
2922
+ code: "resume_file_required",
2923
+ message: "The upload is not finished, so resuming needs the original file."
2924
+ });
2925
+ }
2926
+ function jobMissing() {
2927
+ return new ResumeMismatchError({
2928
+ code: "resume_job_missing",
2929
+ message: "The job behind this resume token no longer exists, so it cannot be resumed."
2930
+ });
2931
+ }
2932
+ function clampLength(size, offset, length) {
2933
+ return Math.max(0, Math.min(length, size - offset));
2934
+ }
2935
+ var MemorySource = class {
2936
+ size;
2937
+ sequential = false;
2938
+ probeInput;
2939
+ #bytes;
2940
+ constructor(bytes) {
2941
+ this.#bytes = bytes;
2942
+ this.size = bytes.byteLength;
2943
+ this.probeInput = { kind: "bytes", bytes };
2944
+ }
2945
+ head(length, signal) {
2946
+ return this.read(0, length, signal);
2947
+ }
2948
+ read(offset, length, signal) {
2949
+ if (signal?.aborted) return Promise.reject(aborted(signal));
2950
+ return Promise.resolve(
2951
+ this.#bytes.subarray(offset, offset + clampLength(this.size, offset, length))
2952
+ );
2953
+ }
2954
+ // Stryker disable next-line BlockStatement: every caller awaits close(), and awaiting undefined is the same as a resolved promise
2955
+ close() {
2956
+ return Promise.resolve();
2957
+ }
2958
+ };
2959
+ var BlobSource = class {
2960
+ size;
2961
+ sequential = false;
2962
+ probeInput;
2963
+ #blob;
2964
+ constructor(blob) {
2965
+ this.#blob = blob;
2966
+ this.size = blob.size;
2967
+ this.probeInput = { kind: "blob", blob: blob.slice(0, blob.size, "") };
2968
+ }
2969
+ head(length, signal) {
2970
+ return this.read(0, length, signal);
2971
+ }
2972
+ async read(offset, length, signal) {
2973
+ if (signal?.aborted) throw aborted(signal);
2974
+ const end = offset + clampLength(this.size, offset, length);
2975
+ const bytes = new Uint8Array(await this.#blob.slice(offset, end).arrayBuffer());
2976
+ if (signal?.aborted) throw aborted(signal);
2977
+ return bytes;
2978
+ }
2979
+ // Stryker disable next-line BlockStatement: every caller awaits close(), and awaiting undefined is the same as a resolved promise
2980
+ close() {
2981
+ return Promise.resolve();
2982
+ }
2983
+ };
2984
+ var FileSource = class {
2985
+ size;
2986
+ sequential = false;
2987
+ probeInput;
2988
+ #handle;
2989
+ constructor(handle, path, size) {
2990
+ this.#handle = handle;
2991
+ this.size = size;
2992
+ this.probeInput = { kind: "path", path };
2993
+ }
2994
+ head(length, signal) {
2995
+ return this.read(0, length, signal);
2996
+ }
2997
+ async read(offset, length, signal) {
2998
+ if (signal?.aborted) throw aborted(signal);
2999
+ const wanted = clampLength(this.size, offset, length);
3000
+ const buffer = new Uint8Array(wanted);
3001
+ let filled = 0;
3002
+ while (filled < wanted) {
3003
+ const { bytesRead } = await this.#handle.read(
3004
+ buffer,
3005
+ filled,
3006
+ wanted - filled,
3007
+ offset + filled
3008
+ );
3009
+ if (signal?.aborted) throw aborted(signal);
3010
+ if (bytesRead === 0) break;
3011
+ filled += bytesRead;
3012
+ }
3013
+ return buffer.subarray(0, filled);
3014
+ }
3015
+ async close() {
3016
+ await this.#handle.close();
3017
+ }
3018
+ };
3019
+ function streamSizeMismatch(message) {
3020
+ return new UploadError({ code: "stream_size_mismatch", message });
3021
+ }
3022
+ var StreamSource = class {
3023
+ size;
3024
+ sequential = true;
3025
+ probeInput = null;
3026
+ #reader;
3027
+ #chunks = [];
3028
+ #buffered = 0;
3029
+ #position = 0;
3030
+ #ended = false;
3031
+ constructor(stream, size) {
3032
+ this.#reader = stream.getReader();
3033
+ this.size = size;
3034
+ }
3035
+ /** One read, given up with the SDK's `AbortError` when `signal` fires; `close()` releases the reader and leaves the stream itself alone. */
3036
+ #next(signal) {
3037
+ const pending = this.#reader.read();
3038
+ if (signal === void 0) return pending;
3039
+ if (signal.aborted) {
3040
+ pending.catch(() => void 0);
3041
+ return Promise.reject(aborted(signal));
3042
+ }
3043
+ return new Promise((resolve, reject) => {
3044
+ const onAbort = () => {
3045
+ reject(aborted(signal));
3046
+ };
3047
+ signal.addEventListener("abort", onAbort);
3048
+ pending.then(resolve, reject).finally(() => {
3049
+ signal.removeEventListener("abort", onAbort);
3050
+ });
3051
+ });
3052
+ }
3053
+ async #fill(length, signal) {
3054
+ while (this.#buffered < length && !this.#ended) {
3055
+ const { done, value } = await this.#next(signal);
3056
+ if (done) {
3057
+ this.#ended = true;
3058
+ } else {
3059
+ this.#chunks.push(value);
3060
+ this.#buffered += value.byteLength;
3061
+ }
3062
+ }
3063
+ }
3064
+ /**
3065
+ * The first `length` buffered bytes in one fresh array. A chunk that is only partly taken keeps a
3066
+ * copy of its rest, so the returned part and the leftover never share (or pin) each other's memory.
3067
+ */
3068
+ #take(length, consume) {
3069
+ const out = new Uint8Array(Math.min(length, this.#buffered));
3070
+ let at = 0;
3071
+ let used = 0;
3072
+ for (const chunk of this.#chunks) {
3073
+ if (at === out.byteLength) break;
3074
+ const n = Math.min(chunk.byteLength, out.byteLength - at);
3075
+ out.set(chunk.subarray(0, n), at);
3076
+ at += n;
3077
+ if (n < chunk.byteLength) {
3078
+ if (consume) this.#chunks[used] = chunk.slice(n);
3079
+ break;
3080
+ }
3081
+ used += 1;
3082
+ }
3083
+ if (consume) {
3084
+ this.#chunks.splice(0, used);
3085
+ this.#buffered -= out.byteLength;
3086
+ }
3087
+ return out;
3088
+ }
3089
+ async head(length, signal) {
3090
+ await this.#fill(length, signal);
3091
+ return this.#take(length, false);
3092
+ }
3093
+ async read(offset, length, signal) {
3094
+ if (offset !== this.#position) {
3095
+ throw new UploadError({
3096
+ code: "stream_not_seekable",
3097
+ message: "A stream can only be uploaded in order, one part at a time."
3098
+ });
3099
+ }
3100
+ const wanted = clampLength(this.size, offset, length);
3101
+ await this.#fill(wanted, signal);
3102
+ if (this.#buffered < wanted) {
3103
+ throw streamSizeMismatch(
3104
+ `The stream ended after ${String(offset + this.#buffered)} bytes; size says ${String(this.size)}.`
3105
+ );
3106
+ }
3107
+ const part = this.#take(wanted, true);
3108
+ this.#position += wanted;
3109
+ if (this.#position === this.size) {
3110
+ await this.#fill(1, signal);
3111
+ if (this.#buffered > 0) {
3112
+ throw streamSizeMismatch(
3113
+ `The stream has more than the ${String(this.size)} bytes size says.`
3114
+ );
3115
+ }
3116
+ }
3117
+ return part;
3118
+ }
3119
+ /** The stream belongs to the caller: releasing the lock leaves it uncanceled, unclosed and readable. */
3120
+ close() {
3121
+ this.#reader.releaseLock();
3122
+ return Promise.resolve();
3123
+ }
3124
+ };
3125
+ function isStream(file) {
3126
+ return typeof file === "object" && "getReader" in file;
3127
+ }
3128
+ function unreadable2(path, cause) {
3129
+ return new PreflightError({
3130
+ code: "file_unreadable",
3131
+ message: `${path} cannot be read: it is missing, not a regular file or cannot be opened${cause === void 0 ? "" : ` (${describeError(cause)})`}.`,
3132
+ cause
3133
+ });
3134
+ }
3135
+ async function statFile(path) {
3136
+ let info;
3137
+ try {
3138
+ info = await stat(path);
3139
+ } catch (error) {
3140
+ throw unreadable2(path, error);
3141
+ }
3142
+ if (!info.isFile()) throw unreadable2(path);
3143
+ return info.size;
3144
+ }
3145
+ async function openFile(path) {
3146
+ await statFile(path);
3147
+ let handle;
3148
+ try {
3149
+ handle = await open(path, "r");
3150
+ } catch (error) {
3151
+ throw unreadable2(path, error);
3152
+ }
3153
+ try {
3154
+ const info = await handle.stat();
3155
+ if (!info.isFile()) throw unreadable2(path);
3156
+ return new FileSource(handle, path, info.size);
3157
+ } catch (error) {
3158
+ await handle.close();
3159
+ throw error instanceof PreflightError ? error : unreadable2(path, error);
3160
+ }
3161
+ }
3162
+ async function openSource(file, size) {
3163
+ if (typeof file === "string") return openFile(file);
3164
+ if (file instanceof Uint8Array) return new MemorySource(file);
3165
+ if (file instanceof Blob) return new BlobSource(file);
3166
+ if (isStream(file)) {
3167
+ if (size === void 0) {
3168
+ throw new PreflightError({
3169
+ code: "invalid_option",
3170
+ message: "A stream input needs size (and durationSeconds); the SDK will not buffer an unbounded stream."
3171
+ });
3172
+ }
3173
+ if (!Number.isSafeInteger(size) || size < 1) {
3174
+ throw new PreflightError({
3175
+ code: "invalid_option",
3176
+ message: "size must be a whole number of bytes, 1 or more."
3177
+ });
3178
+ }
3179
+ return new StreamSource(file, size);
3180
+ }
3181
+ throw new PreflightError({
3182
+ code: "invalid_option",
3183
+ message: "file must be a path, a Blob, a Uint8Array or a ReadableStream."
3184
+ });
3185
+ }
3186
+
3187
+ // src/transcribe/upload.ts
3188
+ var PROXIED_PART_BYTES = MAX_PART_BYTES;
3189
+ var URL_MAX_AGE_MS = 23 * 3600 * 1e3;
3190
+ var PartUrls = class {
3191
+ #api;
3192
+ #cfg;
3193
+ #jobId;
3194
+ #signal;
3195
+ #pages = [];
3196
+ #inflight = /* @__PURE__ */ new Map();
3197
+ #next;
3198
+ #direct = true;
3199
+ /** `signal` is the caller's and the upload's own failure signal combined, so a page fetch stops with the rest. */
3200
+ constructor(settings, plan, signal) {
3201
+ this.#api = settings.api;
3202
+ this.#cfg = settings.cfg;
3203
+ this.#jobId = settings.jobId;
3204
+ this.#signal = signal;
3205
+ this.#next = plan.next_part_number;
3206
+ if (plan.part_urls === null) this.#direct = false;
3207
+ else if (plan.part_urls.length > 0) this.#store(plan.part_urls);
3208
+ }
3209
+ #store(urls) {
3210
+ const start = Math.min(...urls.map((entry) => entry.n));
3211
+ const page = {
3212
+ start,
3213
+ urls: new Map(urls.map((entry) => [entry.n, entry.url])),
3214
+ fetchedAt: this.#cfg.clock.now()
3215
+ };
3216
+ const at = this.#pages.findIndex((existing) => existing.start === start);
3217
+ if (at === -1) this.#pages.push(page);
3218
+ else this.#pages[at] = page;
3219
+ return start;
3220
+ }
3221
+ #pageOf(n) {
3222
+ return this.#pages.find((page) => page.urls.has(n));
3223
+ }
3224
+ /** One request per page start however many parts wait for it; a page without URLs switches the job to proxied parts. */
3225
+ #fetchPage(start) {
3226
+ const running = this.#inflight.get(start);
3227
+ if (running !== void 0) return running;
3228
+ const request = (async () => {
3229
+ const data = await readBody(
3230
+ this.#api.GET("/v1/uploads/{job}/part-urls", {
3231
+ params: { path: { job: this.#jobId }, query: { start } },
3232
+ signal: this.#signal,
3233
+ parseAs: "text"
3234
+ }),
3235
+ "GET /v1/uploads/{job}/part-urls",
3236
+ PART_URLS
3237
+ ).catch((error) => {
3238
+ if (error instanceof NotFoundError) return null;
3239
+ throw error;
3240
+ });
3241
+ const { part_urls: urls, next_part_number: next = null } = data ?? { part_urls: null };
3242
+ if (urls === null || urls.length === 0) {
3243
+ this.#direct = false;
3244
+ return;
3245
+ }
3246
+ const stored = this.#store(urls);
3247
+ if (this.#next === stored) this.#next = next;
3248
+ })().finally(() => {
3249
+ this.#inflight.delete(start);
3250
+ });
3251
+ this.#inflight.set(start, request);
3252
+ return request;
3253
+ }
3254
+ /** The URL for part `n`, or null when the job turns out to take proxied parts only. */
3255
+ async urlFor(n) {
3256
+ for (; ; ) {
3257
+ if (!this.#direct) return null;
3258
+ const page = this.#pageOf(n);
3259
+ if (page === void 0) {
3260
+ if (this.#next === null) {
3261
+ throw new UploadError({
3262
+ code: "part_url_missing",
3263
+ message: `No upload URL for part ${String(n)}.`
3264
+ });
3265
+ }
3266
+ await this.#fetchPage(this.#next);
3267
+ continue;
3268
+ }
3269
+ if (this.#cfg.clock.now() - page.fetchedAt > URL_MAX_AGE_MS) {
3270
+ await this.#fetchPage(page.start);
3271
+ continue;
3272
+ }
3273
+ return page.urls.get(n) ?? null;
3274
+ }
3275
+ }
3276
+ /** Fetches the page `stale` came from again, unless another part already did. */
3277
+ async refresh(n, stale) {
3278
+ const page = this.#pageOf(n);
3279
+ if (page?.urls.get(n) !== stale) return;
3280
+ await this.#fetchPage(page.start);
3281
+ }
3282
+ };
3283
+ function isTerminal(error) {
3284
+ const status = error.status ?? 0;
3285
+ return error instanceof ApiError && status >= 400 && status < 500 && status !== 429;
3286
+ }
3287
+ function endsUpload(error) {
3288
+ return error instanceof UploadError && (error.code === "part_url_missing" || error.code === "size_exceeded");
3289
+ }
3290
+ function etagOf(response) {
3291
+ const etag = response.headers.get("etag")?.replaceAll('"', "") ?? "";
3292
+ if (etag === "") {
3293
+ throw new UploadError({
3294
+ code: "etag_missing",
3295
+ message: "Storage answered a part without an ETag."
3296
+ });
3297
+ }
3298
+ return etag;
3299
+ }
3300
+ async function readPart(source, offset, length, signal) {
3301
+ try {
3302
+ return await source.read(offset, length, signal);
3303
+ } catch (error) {
3304
+ if (error instanceof HushscriptError) throw error;
3305
+ throw new UploadError({
3306
+ code: "source_read_failed",
3307
+ message: `Reading the input failed at byte ${String(offset)}, part way through the upload (${describeError(error)}).`,
3308
+ cause: error
3309
+ });
3310
+ }
3311
+ }
3312
+ async function uploadParts(settings) {
3313
+ const { cfg, jobId, source, plan } = settings;
3314
+ const partSize = plan?.part_size_bytes ?? PROXIED_PART_BYTES;
3315
+ if (!Number.isInteger(partSize) || partSize < 1) {
3316
+ throw new UploadError({
3317
+ code: "bad_part_plan",
3318
+ message: "The API returned an unusable part size."
3319
+ });
3320
+ }
3321
+ const totalParts = Math.ceil(source.size / partSize);
3322
+ if (plan !== null && plan.total_parts !== totalParts) {
3323
+ throw new UploadError({
3324
+ code: "bad_part_plan",
3325
+ message: `The API planned ${String(plan.total_parts)} parts; the file needs ${String(totalParts)}.`
3326
+ });
3327
+ }
3328
+ const failure = new AbortController();
3329
+ const signal = AbortSignal.any([settings.signal, failure.signal]);
3330
+ const urls = plan === null ? null : new PartUrls(settings, plan, signal);
3331
+ const putFetch = hardenedFetch(cfg, "idempotent", { timeoutMs: cfg.partTimeoutMs });
3332
+ const proxiedUrl = (n) => `${cfg.baseUrl}/v1/uploads/${encodeURIComponent(jobId)}/parts/${String(n)}`;
3333
+ const acks = /* @__PURE__ */ new Map();
3334
+ let proxied = plan === null;
3335
+ const isProxied = () => proxied;
3336
+ const isAborted2 = () => signal.aborted;
3337
+ let directSucceeded = false;
3338
+ let sent = 0;
3339
+ let nextPart = 1;
3340
+ const failures = [];
3341
+ const putDirect = async (url2, bytes) => {
3342
+ const response = await putFetch(url2, {
3343
+ method: "PUT",
3344
+ body: bytes,
3345
+ signal
3346
+ });
3347
+ return etagOf(response);
3348
+ };
3349
+ const putProxied = async (n, bytes) => {
3350
+ await putFetch(proxiedUrl(n), {
3351
+ method: "PUT",
3352
+ headers: {
3353
+ authorization: cfg.authorization(),
3354
+ "user-agent": cfg.userAgent,
3355
+ "content-type": "application/octet-stream"
3356
+ },
3357
+ body: bytes,
3358
+ signal
3359
+ });
3360
+ };
3361
+ const recover = async (pages, error, n, url2, state) => {
3362
+ if (!(error instanceof HushscriptError)) throw error;
3363
+ if (isAborted2() || endsUpload(error)) throw error;
3364
+ if (url2 === null && directSucceeded && isTerminal(error)) throw error;
3365
+ let cause = error;
3366
+ const forbidden = error.status === 403;
3367
+ if (url2 !== null && !state.refetched && (forbidden || directSucceeded)) {
3368
+ state.refetched = true;
3369
+ try {
3370
+ await pages.refresh(n, url2);
3371
+ return;
3372
+ } catch (refreshError) {
3373
+ if (!(refreshError instanceof HushscriptError)) throw refreshError;
3374
+ if (isAborted2() || endsUpload(refreshError) || directSucceeded && isTerminal(refreshError))
3375
+ throw refreshError;
3376
+ cause = refreshError;
3377
+ }
3378
+ }
3379
+ if (!directSucceeded) {
3380
+ proxied = true;
3381
+ return;
3382
+ }
3383
+ throw new UploadError({
3384
+ code: "parts_exhausted",
3385
+ message: `Part ${String(n)} failed after retries and a fresh URL.`,
3386
+ cause
3387
+ });
3388
+ };
3389
+ const sendDirect = async (pages, n, bytes) => {
3390
+ const state = {};
3391
+ while (!isProxied()) {
3392
+ let url2;
3393
+ try {
3394
+ url2 = await pages.urlFor(n);
3395
+ } catch (error) {
3396
+ await recover(pages, error, n, null, state);
3397
+ continue;
3398
+ }
3399
+ if (url2 === null) {
3400
+ if (directSucceeded) {
3401
+ throw new UploadError({
3402
+ code: "part_url_missing",
3403
+ message: `No upload URL for part ${String(n)}.`
3404
+ });
3405
+ }
3406
+ proxied = true;
3407
+ return;
3408
+ }
3409
+ try {
3410
+ const etag = await putDirect(url2, bytes);
3411
+ if (isProxied()) return;
3412
+ directSucceeded = true;
3413
+ acks.set(n, etag);
3414
+ return;
3415
+ } catch (error) {
3416
+ await recover(pages, error, n, url2, state);
3417
+ }
3418
+ }
3419
+ };
3420
+ const sendPart = async (n, bytes) => {
3421
+ if (urls !== null) await sendDirect(urls, n, bytes);
3422
+ if (isProxied()) await putProxied(n, bytes);
3423
+ };
3424
+ const worker = async () => {
3425
+ while (!signal.aborted) {
3426
+ const n = nextPart;
3427
+ nextPart += 1;
3428
+ if (n > totalParts) return;
3429
+ const offset = (n - 1) * partSize;
3430
+ const bytes = await readPart(source, offset, partSize, signal);
3431
+ await sendPart(n, bytes);
3432
+ sent += bytes.byteLength;
3433
+ settings.onAcked(sent);
3434
+ }
3435
+ };
3436
+ const guarded2 = async () => {
3437
+ try {
3438
+ await worker();
3439
+ } catch (error) {
3440
+ failures.push(error);
3441
+ failure.abort();
3442
+ }
3443
+ };
3444
+ const workers = source.sequential ? 1 : Math.min(settings.concurrency, totalParts);
3445
+ await Promise.all(Array.from({ length: workers }, guarded2));
3446
+ if (failures.length > 0) throw failures[0];
3447
+ if (settings.signal.aborted) throw aborted(settings.signal);
3448
+ if (isProxied()) return null;
3449
+ return [...acks.entries()].sort(([a], [b]) => a - b).map(([n, etag]) => ({ n, etag }));
3450
+ }
3451
+
3452
+ // src/transcribe/index.ts
3453
+ var DEFAULT_CONCURRENCY = 4;
3454
+ var MAX_CONCURRENCY = 8;
3455
+ function transcriptionOptions(options) {
3456
+ const out = {};
3457
+ if (options.language !== void 0) out.language_code = options.language;
3458
+ if (options.speakers !== void 0) out.speaker_detection = options.speakers;
3459
+ if (options.translateTo !== void 0) out.translation_targets = options.translateTo;
3460
+ const { keyterms, prompt } = normalizeContext(options);
3461
+ if (keyterms.length > 0) out.keyterms_prompt = keyterms;
3462
+ if (prompt !== void 0) out.prompt = prompt;
3463
+ if (options.medical !== void 0) out.medical_mode = options.medical;
3464
+ if (options.multichannel !== void 0) out.multichannel = options.multichannel;
3465
+ if (options.autoInsights !== void 0) out.auto_generate_insights = options.autoInsights;
3466
+ return Object.keys(out).length === 0 ? void 0 : out;
3467
+ }
3468
+ function checkRuntimeOptions(options) {
3469
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
3470
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > MAX_CONCURRENCY) {
3471
+ throw invalidOption(`concurrency must be a whole number from 1 to ${String(MAX_CONCURRENCY)}.`);
3472
+ }
3473
+ const waitTimeout = options.waitTimeout ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
3474
+ if (!Number.isFinite(waitTimeout) || waitTimeout <= 0) {
3475
+ throw invalidOption("waitTimeout must be a positive number of seconds.");
3476
+ }
3477
+ checkDurationOption(options.durationSeconds);
3478
+ return { concurrency, waitTimeout };
3479
+ }
3480
+ var CallbackErrors = class {
3481
+ // Several workers can have a callback throw at once, so every thrown value is kept, not the last.
3482
+ // The set lives as long as one transcribe or resume call.
3483
+ #thrown = /* @__PURE__ */ new Set();
3484
+ wrap(callback) {
3485
+ if (callback === void 0) return void 0;
3486
+ return (value) => {
3487
+ try {
3488
+ callback(value);
3489
+ } catch (error) {
3490
+ this.#thrown.add(error);
3491
+ throw error;
3492
+ }
3493
+ };
3494
+ }
3495
+ owns(error) {
3496
+ return this.#thrown.has(error);
3497
+ }
3498
+ };
3499
+ var Pipeline = class {
3500
+ #cfg;
3501
+ #api;
3502
+ constructor(cfg, api) {
3503
+ this.#cfg = cfg;
3504
+ this.#api = api;
3505
+ }
3506
+ async transcribe(file, options = {}) {
3507
+ const { concurrency, waitTimeout } = checkRuntimeOptions(options);
3508
+ const signal = options.signal ?? new AbortController().signal;
3509
+ const callbackErrors = new CallbackErrors();
3510
+ const onProgress = callbackErrors.wrap(options.onProgress);
3511
+ const onResumeToken = callbackErrors.wrap(options.onResumeToken);
3512
+ let token = null;
3513
+ let sentKey = null;
3514
+ let source = null;
3515
+ try {
3516
+ source = await openSource(file, options.size);
3517
+ if (source.sequential && options.durationSeconds === void 0) {
3518
+ throw invalidOption("A stream input needs durationSeconds and size.");
3519
+ }
3520
+ const durationSeconds = await preflight(source, options, this.#api, signal);
3521
+ const idempotencyKey = options.idempotencyKey ?? randomUUID();
3522
+ onProgress?.({
3523
+ stage: "uploading",
3524
+ percent: 0,
3525
+ bytesSent: 0,
3526
+ bytesTotal: source.size
3527
+ });
3528
+ const body = {
3529
+ size_bytes: source.size,
3530
+ duration_seconds: durationSeconds,
3531
+ idempotency_key: idempotencyKey
3532
+ };
3533
+ if (options.title !== void 0) body.title = options.title;
3534
+ if (options.matterCode !== void 0) body.matter_code = options.matterCode;
3535
+ const transcription = transcriptionOptions(options);
3536
+ if (transcription !== void 0) body.transcription_options = transcription;
3537
+ sentKey = idempotencyKey;
3538
+ const created = await readBody(
3539
+ this.#api.POST("/v1/uploads", { body, signal, parseAs: "text" }),
3540
+ "POST /v1/uploads",
3541
+ UPLOAD_CREATE
3542
+ );
3543
+ const state = {
3544
+ jobId: created.job_id,
3545
+ idempotencyKey,
3546
+ size: source.size,
3547
+ durationSeconds
3548
+ };
3549
+ token = encodeResumeToken(state);
3550
+ onResumeToken?.(token);
3551
+ return await this.#finish(created, {
3552
+ state,
3553
+ token,
3554
+ source,
3555
+ concurrency,
3556
+ waitTimeout,
3557
+ signal,
3558
+ onProgress
3559
+ });
3560
+ } catch (error) {
3561
+ throw this.#annotate(error, token, signal, sentKey, callbackErrors);
3562
+ } finally {
3563
+ await source?.close();
3564
+ }
3565
+ }
3566
+ async resume(token, file, options = {}) {
3567
+ const state = decodeResumeToken(token);
3568
+ const { concurrency, waitTimeout } = checkRuntimeOptions(options);
3569
+ const signal = options.signal ?? new AbortController().signal;
3570
+ const callbackErrors = new CallbackErrors();
3571
+ const onProgress = callbackErrors.wrap(options.onProgress);
3572
+ let source = null;
3573
+ try {
3574
+ if (typeof file === "string") {
3575
+ const size = await statFile(file).catch(() => null);
3576
+ if (size !== null) assertSameFile(state, size);
3577
+ } else if (file !== void 0) {
3578
+ source = await openSource(file, state.size);
3579
+ assertSameFile(state, source.size);
3580
+ }
3581
+ const run = {
3582
+ state,
3583
+ token,
3584
+ source,
3585
+ concurrency,
3586
+ waitTimeout,
3587
+ signal,
3588
+ onProgress
3589
+ };
3590
+ const job = await readJob(this.#api, state.jobId, signal).catch((error) => {
3591
+ throw error instanceof NotFoundError ? jobMissing() : error;
3592
+ });
3593
+ if (job.state !== "uploading")
3594
+ return await this.#finish({ state: job.state, upload: null }, run, job);
3595
+ if (file === void 0) throw fileRequired();
3596
+ if (source === null) {
3597
+ source = await openSource(file, state.size);
3598
+ assertSameFile(state, source.size);
3599
+ run.source = source;
3600
+ }
3601
+ onProgress?.({
3602
+ stage: "uploading",
3603
+ percent: 0,
3604
+ bytesSent: 0,
3605
+ bytesTotal: source.size
3606
+ });
3607
+ const created = await readBody(
3608
+ this.#api.POST("/v1/uploads", {
3609
+ body: {
3610
+ size_bytes: state.size,
3611
+ duration_seconds: Math.ceil(state.durationSeconds),
3612
+ idempotency_key: state.idempotencyKey
3613
+ },
3614
+ signal,
3615
+ parseAs: "text"
3616
+ }),
3617
+ "POST /v1/uploads",
3618
+ UPLOAD_CREATE
3619
+ );
3620
+ if (created.job_id !== state.jobId) throw jobMissing();
3621
+ return await this.#finish(created, run);
3622
+ } catch (error) {
3623
+ throw this.#annotate(error, token, signal, state.idempotencyKey, callbackErrors);
3624
+ } finally {
3625
+ await source?.close();
3626
+ }
3627
+ }
3628
+ async waitForJob(id, options = {}) {
3629
+ const { waitTimeout } = checkRuntimeOptions({ waitTimeout: options.waitTimeout });
3630
+ return waitForJob({
3631
+ api: this.#api,
3632
+ clock: this.#cfg.clock,
3633
+ jobId: id,
3634
+ waitTimeout,
3635
+ signal: options.signal ?? new AbortController().signal,
3636
+ onProgress: options.onProgress,
3637
+ resumeToken: null
3638
+ });
3639
+ }
3640
+ /**
3641
+ * After a create was sent, adds the key to every error and the resume token to a cancellation.
3642
+ * An error from before the create (there is no token yet either), or one a caller's callback
3643
+ * threw, goes back as it was.
3644
+ */
3645
+ #annotate(error, token, signal, key, callbackErrors) {
3646
+ if (key === null || callbackErrors.owns(error)) return error;
3647
+ const cancelled = error instanceof AbortError || signal.aborted;
3648
+ const out = cancelled && token !== null ? new AbortError({
3649
+ message: error instanceof Error ? error.message : "The request was aborted.",
3650
+ cause: error,
3651
+ resumeToken: token
3652
+ }) : asHushscriptError(error);
3653
+ out.idempotencyKey = key;
3654
+ out.resumeToken ??= token;
3655
+ return out;
3656
+ }
3657
+ /** `known` is the job as `resume()` just read it, for a job that is not uploading. */
3658
+ async #finish(created, run, known = null) {
3659
+ const { state, signal } = run;
3660
+ let jobState = created.state;
3661
+ let finished = known;
3662
+ if (created.state === "uploading") {
3663
+ if (run.source === null) throw fileRequired();
3664
+ jobState = await this.#upload(created, run, run.source);
3665
+ finished = null;
3666
+ }
3667
+ if (jobState === "done" || jobState === "failed") {
3668
+ return await this.#fetch(finished ?? await readJob(this.#api, state.jobId, signal), run);
3669
+ }
3670
+ if (jobState === "queued" || jobState === "processing") {
3671
+ run.onProgress?.({ stage: "processing", state: jobState });
3672
+ }
3673
+ const job = await waitForJob({
3674
+ api: this.#api,
3675
+ clock: this.#cfg.clock,
3676
+ jobId: state.jobId,
3677
+ waitTimeout: run.waitTimeout,
3678
+ signal,
3679
+ onProgress: run.onProgress,
3680
+ resumeToken: run.token
3681
+ });
3682
+ return await this.#fetch(job, run);
3683
+ }
3684
+ async #upload(created, run, source) {
3685
+ const { state } = run;
3686
+ const stop = new AbortController();
3687
+ const signal = AbortSignal.any([run.signal, stop.signal]);
3688
+ const failure = {};
3689
+ const heartbeat = runHeartbeat({
3690
+ clock: this.#cfg.clock,
3691
+ signal,
3692
+ send: () => this.#api.POST("/v1/uploads/{job}/heartbeat", {
3693
+ params: { path: { job: state.jobId } },
3694
+ signal
3695
+ })
3696
+ }).catch((error) => {
3697
+ failure.failed = true;
3698
+ failure.error = error;
3699
+ stop.abort();
3700
+ });
3701
+ try {
3702
+ const acks = await uploadParts({
3703
+ cfg: this.#cfg,
3704
+ api: this.#api,
3705
+ jobId: state.jobId,
3706
+ source,
3707
+ plan: created.upload,
3708
+ concurrency: run.concurrency,
3709
+ signal,
3710
+ onAcked: (bytesSent) => {
3711
+ run.onProgress?.({
3712
+ stage: "uploading",
3713
+ percent: Math.floor(bytesSent * 100 / source.size),
3714
+ bytesSent,
3715
+ bytesTotal: source.size
3716
+ });
3717
+ }
3718
+ });
3719
+ const completed = await readBody(
3720
+ this.#api.POST("/v1/uploads/{job}/complete", {
3721
+ params: { path: { job: state.jobId } },
3722
+ ...acks === null ? {} : { body: { parts: acks } },
3723
+ signal,
3724
+ parseAs: "text"
3725
+ }),
3726
+ "POST /v1/uploads/{job}/complete",
3727
+ UPLOAD_COMPLETE
3728
+ );
3729
+ return completed.state;
3730
+ } catch (error) {
3731
+ throw failure.failed ? failure.error : error;
3732
+ } finally {
3733
+ stop.abort();
3734
+ await heartbeat;
3735
+ }
3736
+ }
3737
+ async #fetch(job, run) {
3738
+ if (job.state === "failed") throw failureOf(job);
3739
+ if (job.transcript_id === null) {
3740
+ throw new UploadError({
3741
+ code: "transcript_missing",
3742
+ message: "The job is done but has no transcript id."
3743
+ });
3744
+ }
3745
+ return Transcript.fromApi(await readTranscript(this.#api, job.transcript_id, run.signal));
3746
+ }
3747
+ };
3748
+
3749
+ // src/client.ts
3750
+ var Hushscript = class {
3751
+ /** Every `/v1` operation, typed from the OpenAPI document, with the SDK's retries, timeouts and redaction. */
3752
+ api;
3753
+ /** Jobs, including those still processing; `wait` polls until one is done or failed. */
3754
+ jobs;
3755
+ /** Finished transcripts: list, read and the deletion records. */
3756
+ transcripts;
3757
+ /** Packs, saved cards and purchase history. Buying also needs the token's own purchase switch and the account-wide one, both in the app under Account → MCP. */
3758
+ billing;
3759
+ /** Calls an agent account makes with its own token. */
3760
+ agent;
3761
+ /** Signup and claim for agent accounts: no token needed, so they are static. */
3762
+ static agent = agentStatic;
3763
+ #config;
3764
+ #pipeline;
3765
+ constructor(options = {}) {
3766
+ const config = resolveConfig(options);
3767
+ this.#config = config;
3768
+ Object.defineProperty(this, inspect.custom, { value: () => this.toString() });
3769
+ const api = createClient({ baseUrl: config.baseUrl, fetch: createTransport(config) });
3770
+ api.use({
3771
+ onRequest: ({ request }) => {
3772
+ request.headers.set("authorization", config.authorization());
3773
+ request.headers.set("user-agent", config.userAgent);
3774
+ }
3775
+ });
3776
+ this.api = guardApi(api);
3777
+ this.#pipeline = new Pipeline(config, this.api);
3778
+ this.jobs = new JobsResource(this.api, (id, options2) => this.#pipeline.waitForJob(id, options2));
3779
+ this.transcripts = new TranscriptsResource(this.api);
3780
+ this.billing = new BillingResource(this.api);
3781
+ this.agent = new AgentResource(this.api, config);
3782
+ }
3783
+ /** Uploads `file`, waits for the transcript and returns it. */
3784
+ transcribe(file, options) {
3785
+ return this.#pipeline.transcribe(file, options);
3786
+ }
3787
+ /** Finishes a transcription from the token an `AbortError`, `WaitTimeoutError` or `onResumeToken` gave. */
3788
+ resume(token, file, options) {
3789
+ return this.#pipeline.resume(token, file, options);
3790
+ }
3791
+ /** Buys a pack with the saved default card and returns what the purchase left you with (alias of `hs.billing.buy`). */
3792
+ buy(packId, options) {
3793
+ return this.billing.buy(packId, options);
3794
+ }
3795
+ /** What can be spent now. Held minutes are already deducted from it, so do not subtract them again. */
3796
+ async balance() {
3797
+ return toBalance(await readAccount(this.api));
3798
+ }
3799
+ /** The account in camelCase: balance, holds, expiry, limits, `cardOnFile` and `dataRegion`. */
3800
+ async account() {
3801
+ return toAccount(await readAccount(this.api));
3802
+ }
3803
+ toJSON() {
3804
+ return { token: REDACTED, baseUrl: this.#config.baseUrl };
3805
+ }
3806
+ toString() {
3807
+ return `Hushscript ${JSON.stringify(this.toJSON())}`;
3808
+ }
3809
+ };
3810
+
3811
+ export { AbortError, ApiError, AuthenticationError, ConfigurationError, ConflictError, DailyLimitError, ERROR_CATALOG, FileLimitError, HINTS, Hushscript, HushscriptError, InsufficientBalanceError, JobFailedError, NetworkError, NotFoundError, OPERATION_SCOPES, OutcomeUnknownError, PaymentMethodRequiredError, PermissionDeniedError, PreflightError, RateLimitError, ResumeMismatchError, Transcript, UnsupportedMediaError, UploadError, ValidationError, WaitTimeoutError, paginate };