@aithos/sdk 0.1.0-alpha.6 → 0.1.0-alpha.60

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.
Files changed (105) hide show
  1. package/README.md +202 -7
  2. package/dist/src/agent-dispatch.d.ts +18 -0
  3. package/dist/src/agent-dispatch.js +178 -0
  4. package/dist/src/agent-loop.d.ts +94 -0
  5. package/dist/src/agent-loop.js +95 -0
  6. package/dist/src/agent-tools.d.ts +24 -0
  7. package/dist/src/agent-tools.js +147 -0
  8. package/dist/src/apps.d.ts +224 -0
  9. package/dist/src/apps.js +432 -0
  10. package/dist/src/assets.d.ts +225 -0
  11. package/dist/src/assets.js +534 -0
  12. package/dist/src/auth-api.d.ts +219 -0
  13. package/dist/src/auth-api.js +248 -0
  14. package/dist/src/auth.d.ts +591 -0
  15. package/dist/src/auth.js +947 -31
  16. package/dist/src/compute.d.ts +674 -6
  17. package/dist/src/compute.js +968 -20
  18. package/dist/src/data-schema-contacts-v1.d.ts +14 -0
  19. package/dist/src/data-schema-contacts-v1.js +28 -0
  20. package/dist/src/data.d.ts +368 -0
  21. package/dist/src/data.js +1124 -0
  22. package/dist/src/endpoints.d.ts +43 -0
  23. package/dist/src/endpoints.js +23 -0
  24. package/dist/src/ethos.d.ts +85 -0
  25. package/dist/src/ethos.js +463 -7
  26. package/dist/src/index.d.ts +22 -4
  27. package/dist/src/index.js +47 -2
  28. package/dist/src/internal/cmk-wrap.d.ts +41 -0
  29. package/dist/src/internal/cmk-wrap.js +132 -0
  30. package/dist/src/internal/delegate-bundle.js +7 -2
  31. package/dist/src/internal/envelope.d.ts +93 -0
  32. package/dist/src/internal/envelope.js +59 -0
  33. package/dist/src/internal/owner-signers.d.ts +5 -2
  34. package/dist/src/internal/owner-signers.js +22 -1
  35. package/dist/src/internal/recovery-file.d.ts +2 -0
  36. package/dist/src/internal/recovery-file.js +7 -0
  37. package/dist/src/key-store.d.ts +10 -0
  38. package/dist/src/key-store.js +6 -0
  39. package/dist/src/mandates.d.ts +58 -1
  40. package/dist/src/mandates.js +46 -3
  41. package/dist/src/migrate.d.ts +105 -0
  42. package/dist/src/migrate.js +367 -0
  43. package/dist/src/react/AithosAsset.d.ts +66 -0
  44. package/dist/src/react/AithosAsset.js +67 -0
  45. package/dist/src/react/context.d.ts +29 -0
  46. package/dist/src/react/context.js +31 -0
  47. package/dist/src/react/index.d.ts +29 -0
  48. package/dist/src/react/index.js +31 -0
  49. package/dist/src/react/use-aithos-asset.d.ts +39 -0
  50. package/dist/src/react/use-aithos-asset.js +118 -0
  51. package/dist/src/react/use-transcribe-pending.d.ts +21 -0
  52. package/dist/src/react/use-transcribe-pending.js +47 -0
  53. package/dist/src/rotate.d.ts +94 -0
  54. package/dist/src/rotate.js +298 -0
  55. package/dist/src/sdk.d.ts +36 -2
  56. package/dist/src/sdk.js +72 -1
  57. package/dist/src/transcribe-resilience.d.ts +57 -0
  58. package/dist/src/transcribe-resilience.js +203 -0
  59. package/dist/src/web.d.ts +279 -0
  60. package/dist/src/web.js +186 -0
  61. package/dist/test/agent-dispatch.test.d.ts +2 -0
  62. package/dist/test/agent-dispatch.test.js +222 -0
  63. package/dist/test/agent-loop.test.d.ts +2 -0
  64. package/dist/test/agent-loop.test.js +117 -0
  65. package/dist/test/agent-tools.test.d.ts +2 -0
  66. package/dist/test/agent-tools.test.js +50 -0
  67. package/dist/test/auth-j3.test.js +32 -1
  68. package/dist/test/canonical-conformance.test.d.ts +2 -0
  69. package/dist/test/canonical-conformance.test.js +86 -0
  70. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  71. package/dist/test/compute-delegate-path.test.js +183 -0
  72. package/dist/test/compute.test.js +4 -0
  73. package/dist/test/converse.test.d.ts +2 -0
  74. package/dist/test/converse.test.js +162 -0
  75. package/dist/test/data-sphere.test.d.ts +2 -0
  76. package/dist/test/data-sphere.test.js +57 -0
  77. package/dist/test/endpoints.test.js +40 -1
  78. package/dist/test/envelope-core-conformance.test.d.ts +2 -0
  79. package/dist/test/envelope-core-conformance.test.js +75 -0
  80. package/dist/test/envelope.test.d.ts +2 -0
  81. package/dist/test/envelope.test.js +318 -0
  82. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  83. package/dist/test/ethos-first-edition.test.js +371 -0
  84. package/dist/test/invoke-turn-sdk.test.d.ts +2 -0
  85. package/dist/test/invoke-turn-sdk.test.js +177 -0
  86. package/dist/test/migrate.test.d.ts +2 -0
  87. package/dist/test/migrate.test.js +340 -0
  88. package/dist/test/owner-data-client.test.d.ts +2 -0
  89. package/dist/test/owner-data-client.test.js +88 -0
  90. package/dist/test/rotate-ethos.test.d.ts +2 -0
  91. package/dist/test/rotate-ethos.test.js +151 -0
  92. package/dist/test/rotate.test.d.ts +2 -0
  93. package/dist/test/rotate.test.js +63 -0
  94. package/dist/test/schema-autoresolve.test.d.ts +2 -0
  95. package/dist/test/schema-autoresolve.test.js +146 -0
  96. package/dist/test/sdk.test.js +11 -2
  97. package/dist/test/signup-bootstrap.test.d.ts +2 -0
  98. package/dist/test/signup-bootstrap.test.js +311 -0
  99. package/dist/test/transcribe-invoke.test.d.ts +2 -0
  100. package/dist/test/transcribe-invoke.test.js +204 -0
  101. package/dist/test/transcribe.test.d.ts +2 -0
  102. package/dist/test/transcribe.test.js +186 -0
  103. package/dist/test/web.test.d.ts +2 -0
  104. package/dist/test/web.test.js +270 -0
  105. package/package.json +20 -3
@@ -16,10 +16,18 @@
16
16
  //
17
17
  // MVP scope: single-shot invocation. Multi-turn agentic loops (with native
18
18
  // tool calling on the proxy) will land in a follow-up.
19
- import { buildSignedEnvelope } from "@aithos/protocol-client";
19
+ import { buildSignedEnvelope, } from "@aithos/protocol-client";
20
20
  import { computeInvokeUrl, } from "./endpoints.js";
21
- import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
21
+ import { delegateKeyPair, ownerKeyPair, } from "./internal/protocol-client-bridge.js";
22
22
  import { AithosSDKError } from "./types.js";
23
+ import { LocalPendingTranscribeTracker, TranscribeDraftStore, } from "./transcribe-resilience.js";
24
+ import { runAgenticLoopLocal, } from "./agent-loop.js";
25
+ import { selectAgentTools } from "./agent-tools.js";
26
+ import { dispatchAgentToolLocal, } from "./agent-dispatch.js";
27
+ import { EthosNamespace } from "./ethos.js";
28
+ /** Default / hard cap on client-side loop iterations (mirrors the proxy). */
29
+ const DEFAULT_LOCAL_MAX_ITERATIONS = 6;
30
+ const HARD_LOCAL_MAX_ITERATIONS = 12;
23
31
  /**
24
32
  * `sdk.compute` namespace. Constructed once by the {@link AithosSDK}
25
33
  * constructor; reads the active owner from the supplied
@@ -28,47 +36,831 @@ import { AithosSDKError } from "./types.js";
28
36
  */
29
37
  export class ComputeNamespace {
30
38
  #deps;
39
+ // Lazily-created browser resilience helpers (see transcribe-resilience.ts).
40
+ // Created on first access only — the core invoke path never touches them.
41
+ #pendingTracker = null;
42
+ #draftStore = null;
31
43
  constructor(deps) {
32
44
  this.#deps = deps;
33
45
  }
46
+ #tracker() {
47
+ if (!this.#pendingTracker)
48
+ this.#pendingTracker = new LocalPendingTranscribeTracker();
49
+ return this.#pendingTracker;
50
+ }
51
+ #draft() {
52
+ if (!this.#draftStore)
53
+ this.#draftStore = new TranscribeDraftStore();
54
+ return this.#draftStore;
55
+ }
34
56
  /**
35
57
  * Invoke a Bedrock model through the compute proxy. See
36
58
  * {@link InvokeBedrockArgs} and {@link InvokeBedrockResult}.
37
59
  *
60
+ * Two signer paths are supported:
61
+ *
62
+ * - **Owner**: when the caller is signed in as an owner, the
63
+ * envelope is signed with the owner's `#public` sphere key and
64
+ * no mandate is attached (the proxy resolves the mandate
65
+ * server-side from `params.mandate_id`).
66
+ * - **Delegate**: when the caller is delegate-only (mandate
67
+ * imported via `auth.importMandate`), the envelope is signed
68
+ * with the delegate's bound keypair and the full SignedMandate
69
+ * is attached so the proxy can verify both signature and
70
+ * authorisation in one pass. The mandate must carry the
71
+ * `compute.invoke` scope and the proxy enforces its constraints
72
+ * (caps, allowed models, …) at server-side.
73
+ *
74
+ * Owner takes precedence: if a session has BOTH an owner and a
75
+ * matching delegate session, we use the owner key (more flexible —
76
+ * the mandate is dereferenced via `mandate_id` and there's no
77
+ * lifetime cliff if the delegate seed has been wiped).
78
+ *
38
79
  * @throws {AithosSDKError} on protocol errors. The `code` field is one of
39
- * `sdk_no_owner`, `network`, `http`, `empty`, or any code returned by
40
- * the proxy (`quota_exceeded`, `mandate_revoked`, `insufficient_credits`,
41
- * …).
80
+ * `sdk_no_signer`, `sdk_no_delegate_for_mandate`, `network`, `http`,
81
+ * `empty`, or any code returned by the proxy (`quota_exceeded`,
82
+ * `mandate_revoked`, `insufficient_credits`, …).
42
83
  */
43
84
  async invokeBedrock(args) {
44
- const { auth, appDid, endpoints, fetch: fetchImpl } = this.#deps;
45
- const owner = auth._getOwnerSigners();
46
- if (!owner || owner.destroyed) {
47
- throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in via auth.signIn / signUp / signInWithGoogle / signInWithRecovery first");
48
- }
49
- const publicKp = ownerKeyPair(owner, "public");
85
+ const { endpoints, fetch: fetchImpl } = this.#deps;
86
+ const choice = this.#resolveSigner(args.mandateId);
87
+ const url = computeInvokeUrl(endpoints);
88
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
89
+ const params = {
90
+ app_did: this.#deps.appDid,
91
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
92
+ model: args.model,
93
+ messages: args.messages,
94
+ idempotency_key: idempotencyKey,
95
+ };
96
+ if (args.system !== undefined)
97
+ params.system = args.system;
98
+ if (args.maxTokens !== undefined)
99
+ params.max_tokens = args.maxTokens;
100
+ if (args.temperature !== undefined)
101
+ params.temperature = args.temperature;
102
+ return await this.#signAndPost({
103
+ url,
104
+ method: "aithos.compute_invoke",
105
+ params,
106
+ choice,
107
+ fetchImpl,
108
+ signal: args.signal,
109
+ });
110
+ }
111
+ /**
112
+ * Run an agentic conversation: a multi-turn Bedrock tool-calling loop that
113
+ * runs server-side in a single POST and is billed once for the cumulative
114
+ * token usage. Tools come from the Aithos MCP (declared via `mcp`); the
115
+ * model reads private user data from the client-decrypted `workingSet`.
116
+ *
117
+ * The loop, tool dispatch, and billing all happen on the proxy — the SDK
118
+ * makes ONE signed request and gets the final answer. Same signer paths as
119
+ * {@link invokeBedrock} (owner direct or delegate-under-mandate).
120
+ *
121
+ * @throws {AithosSDKError} `sdk_no_signer`, `sdk_no_delegate_for_mandate`,
122
+ * `network`, `http`, `empty`, or any proxy code (`quota_exceeded`,
123
+ * `mandate_revoked`, `insufficient_credits`, …).
124
+ */
125
+ async runConversation(args) {
126
+ const { endpoints, fetch: fetchImpl } = this.#deps;
127
+ const choice = this.#resolveSigner(args.mandateId);
50
128
  const url = computeInvokeUrl(endpoints);
51
129
  const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
52
130
  const params = {
53
- app_did: appDid,
54
- mandate_id: args.mandateId,
131
+ app_did: this.#deps.appDid,
132
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
55
133
  model: args.model,
56
134
  messages: args.messages,
57
135
  idempotency_key: idempotencyKey,
58
136
  };
137
+ if (args.system !== undefined)
138
+ params.system = args.system;
139
+ if (args.mcp !== undefined)
140
+ params.mcp = args.mcp;
141
+ if (args.workingSet !== undefined)
142
+ params.working_set = args.workingSet;
143
+ if (args.maxTokens !== undefined)
144
+ params.max_tokens = args.maxTokens;
145
+ if (args.maxIterations !== undefined)
146
+ params.max_iterations = args.maxIterations;
147
+ if (args.temperature !== undefined)
148
+ params.temperature = args.temperature;
149
+ return await this.#signAndPost({
150
+ url,
151
+ method: "aithos.compute_converse",
152
+ params,
153
+ choice,
154
+ fetchImpl,
155
+ signal: args.signal,
156
+ });
157
+ }
158
+ /**
159
+ * Run ONE Bedrock turn with tool-calling through the proxy
160
+ * (`aithos.compute_invoke_turn`). Returns the raw content blocks — including
161
+ * any `tool_use` — so the caller can dispatch tools and loop. This is the
162
+ * per-turn primitive the CLIENT-SIDE agentic loop is built on; most callers
163
+ * want {@link runConversationLocal} instead, which drives the loop and
164
+ * dispatches Aithos tools locally.
165
+ *
166
+ * Same signer paths and billing as {@link invokeBedrock}; billed once per
167
+ * turn.
168
+ */
169
+ async invokeTurn(args) {
170
+ const { endpoints, fetch: fetchImpl } = this.#deps;
171
+ const choice = this.#resolveSigner(args.mandateId);
172
+ const url = computeInvokeUrl(endpoints);
173
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
174
+ const params = {
175
+ app_did: this.#deps.appDid,
176
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
177
+ model: args.model,
178
+ messages: args.messages,
179
+ tools: args.tools,
180
+ idempotency_key: idempotencyKey,
181
+ };
182
+ if (args.system !== undefined)
183
+ params.system = args.system;
184
+ if (args.maxTokens !== undefined)
185
+ params.max_tokens = args.maxTokens;
186
+ if (args.temperature !== undefined)
187
+ params.temperature = args.temperature;
188
+ return await this.#signAndPost({
189
+ url,
190
+ method: "aithos.compute_invoke_turn",
191
+ params,
192
+ choice,
193
+ fetchImpl,
194
+ signal: args.signal,
195
+ });
196
+ }
197
+ /**
198
+ * Run a CLIENT-SIDE agentic conversation with tool-calling: a multi-turn
199
+ * loop where each turn is one signed proxy call ({@link invokeTurn}) and the
200
+ * tools the model requests are dispatched LOCALLY against the user's own
201
+ * ethos (reads decrypt locally; writes stage + sign + publish locally). The
202
+ * proxy does pure per-turn inference and never holds a decryption key — keys,
203
+ * data, and dispatch all stay on the client (HANDOFF-AGENT-WRITE-MODE.md
204
+ * §1/§3).
205
+ *
206
+ * Authorisation: an owner has full authority over their own ethos; a
207
+ * delegate is bounded by the mandate's `ethos.read.*` / `ethos.write.*`
208
+ * scopes (a tool out of scope returns an error to the model — nothing is
209
+ * published). The spend capability (`compute.invoke`) is checked server-side
210
+ * on every turn.
211
+ *
212
+ * Billing is PER-TURN cumulative: each turn is billed once and the result's
213
+ * `creditsCharged` is the sum across turns.
214
+ *
215
+ * @throws {AithosSDKError} `sdk_no_signer`, `sdk_no_delegate_for_mandate`,
216
+ * `network`, `http`, `empty`, or any proxy code. Tool-level failures do
217
+ * NOT throw — they are fed back to the model as errors.
218
+ */
219
+ async runConversationLocal(args) {
220
+ // Resolve the subject whose ethos the agent operates on.
221
+ const subjectDid = this.#resolveSubjectDid(args.subjectDid, args.mandateId);
222
+ // Resolve the ethos client + the delegate scopes that bound writes.
223
+ const ethosNs = this.#ethosNamespace();
224
+ const ethosClient = await ethosNs.of(subjectDid);
225
+ const delegateScopes = ethosClient.mode === "delegate"
226
+ ? this.#delegateScopesForSubject(subjectDid)
227
+ : [];
228
+ const dispatchCtx = {
229
+ ethos: ethosClient,
230
+ delegateScopes,
231
+ ...(args.dataProvider ? { dataProvider: args.dataProvider } : {}),
232
+ };
233
+ const tools = selectAgentTools({
234
+ ...(args.tools ? { tools: args.tools } : {}),
235
+ ...(args.readOnly ? { readOnly: true } : {}),
236
+ });
237
+ const maxIterations = Math.max(1, Math.min(HARD_LOCAL_MAX_ITERATIONS, args.maxIterations ?? DEFAULT_LOCAL_MAX_ITERATIONS));
238
+ // Carry the last turn's billing metadata out of the loop.
239
+ let lastAuditId = "";
240
+ let lastFundedBy;
241
+ let lastReceiptId;
242
+ const initialMessages = args.messages.map((m) => ({
243
+ role: m.role,
244
+ content: m.content,
245
+ }));
246
+ const loop = await runAgenticLoopLocal({
247
+ messages: initialMessages,
248
+ maxIterations,
249
+ invokeTurn: async (messages) => {
250
+ const r = await this.invokeTurn({
251
+ ...(args.mandateId !== undefined ? { mandateId: args.mandateId } : {}),
252
+ model: args.model,
253
+ messages,
254
+ tools,
255
+ ...(args.system !== undefined ? { system: args.system } : {}),
256
+ ...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
257
+ ...(args.temperature !== undefined ? { temperature: args.temperature } : {}),
258
+ ...(args.signal ? { signal: args.signal } : {}),
259
+ });
260
+ lastAuditId = r.auditId;
261
+ lastFundedBy = r.fundedBy;
262
+ lastReceiptId = r.receiptId;
263
+ return {
264
+ content: r.content,
265
+ stopReason: r.stopReason,
266
+ usage: r.usage,
267
+ creditsCharged: r.creditsCharged,
268
+ walletBalance: r.walletBalance,
269
+ };
270
+ },
271
+ dispatch: (name, input) => dispatchAgentToolLocal(dispatchCtx, name, input),
272
+ });
273
+ return {
274
+ content: loop.finalContent,
275
+ stopReason: loop.stopReason,
276
+ iterations: loop.iterations,
277
+ usage: loop.usage,
278
+ toolCalls: loop.toolCalls,
279
+ creditsCharged: loop.creditsCharged,
280
+ walletBalance: loop.walletBalance,
281
+ auditId: lastAuditId,
282
+ ...(lastFundedBy ? { fundedBy: lastFundedBy } : {}),
283
+ ...(lastReceiptId ? { receiptId: lastReceiptId } : {}),
284
+ };
285
+ }
286
+ /**
287
+ * Multimodal Bedrock invoke — image + text → text response.
288
+ * Default model: `claude-sonnet-4-6` (vision-capable, reliable JSON).
289
+ *
290
+ * Use when you need a VLM to reason about an image: locating
291
+ * features, structured extraction, semantic Q&A. Prompt the model
292
+ * to return JSON if you need structured output (the API itself is
293
+ * unstructured).
294
+ */
295
+ async invokeBedrockVision(args) {
296
+ const { endpoints, fetch: fetchImpl } = this.#deps;
297
+ const choice = this.#resolveSigner(args.mandateId);
298
+ let imageBase64;
299
+ let imageContentType;
300
+ if ("base64" in args.image) {
301
+ imageBase64 = args.image.base64;
302
+ imageContentType = args.image.contentType;
303
+ }
304
+ else {
305
+ const buf = await args.image.arrayBuffer();
306
+ imageBase64 = arrayBufferToBase64(buf);
307
+ imageContentType = args.image.type || "image/png";
308
+ }
309
+ const url = computeInvokeUrl(endpoints);
310
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
311
+ const model = args.model ?? "claude-sonnet-4-6";
312
+ const params = {
313
+ app_did: this.#deps.appDid,
314
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
315
+ model,
316
+ image_base64: imageBase64,
317
+ image_content_type: imageContentType,
318
+ prompt: args.prompt,
319
+ idempotency_key: idempotencyKey,
320
+ };
59
321
  if (args.system !== undefined)
60
322
  params.system = args.system;
61
323
  if (args.maxTokens !== undefined)
62
324
  params.max_tokens = args.maxTokens;
63
325
  if (args.temperature !== undefined)
64
326
  params.temperature = args.temperature;
327
+ return await this.#signAndPost({
328
+ url,
329
+ method: "aithos.compute_invoke_bedrock_vision",
330
+ params,
331
+ choice,
332
+ fetchImpl,
333
+ signal: args.signal,
334
+ });
335
+ }
336
+ /**
337
+ * Generate one or more images through the Aithos compute proxy
338
+ * (currently powered by fal.ai FLUX models). Spec mirror of
339
+ * {@link invokeBedrock}: same envelope, same wallet path, same
340
+ * mandate-scope gate (`compute.invoke` + `allowed_models`). The
341
+ * separation at the JSON-RPC method level (`aithos.compute_invoke_image`
342
+ * vs `aithos.compute_invoke`) commits each envelope to a specific
343
+ * modality so a stolen text-invoke envelope cannot be replayed
344
+ * against the image endpoint.
345
+ *
346
+ * Default model: `"image:flux-pro-1.1"`. Default aspect ratio: 1:1.
347
+ * Default count: 1 image.
348
+ *
349
+ * Pricing is per image and deterministic (no token-based reconcile):
350
+ * - flux-schnell: 3 000 mc + fee per image
351
+ * - flux-dev: 25 000 mc + fee per image
352
+ * - flux-pro-1.1: 40 000 mc + fee per image
353
+ * - flux-pro-1.1-ultra: 60 000 mc + fee per image
354
+ */
355
+ async invokeImage(args) {
356
+ const { endpoints, fetch: fetchImpl } = this.#deps;
357
+ const choice = this.#resolveSigner(args.mandateId);
358
+ const url = computeInvokeUrl(endpoints);
359
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
360
+ // Default is Imagen 4 since alpha.17 — flat-illustration style is
361
+ // the right match for brand-mascot use cases. FLUX Pro 1.1 remains
362
+ // available via explicit `model: "image:flux-pro-1.1"`.
363
+ const model = args.model ?? "image:imagen-4";
364
+ const params = {
365
+ app_did: this.#deps.appDid,
366
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
367
+ model,
368
+ prompt: args.prompt,
369
+ idempotency_key: idempotencyKey,
370
+ };
371
+ if (args.negativePrompt !== undefined)
372
+ params.negative_prompt = args.negativePrompt;
373
+ if (args.aspectRatio !== undefined)
374
+ params.aspect_ratio = args.aspectRatio;
375
+ if (args.seed !== undefined)
376
+ params.seed = args.seed;
377
+ if (args.numberOfImages !== undefined)
378
+ params.number_of_images = args.numberOfImages;
379
+ return await this.#signAndPost({
380
+ url,
381
+ method: "aithos.compute_invoke_image",
382
+ params,
383
+ choice,
384
+ fetchImpl,
385
+ signal: args.signal,
386
+ });
387
+ }
388
+ /**
389
+ * Run text-prompted segmentation (Florence-2 referring-expression)
390
+ * on a source image. Returns one or more polygons hugging the
391
+ * region matching the text prompt.
392
+ *
393
+ * Use cases: locate the chest/torso area of a generated mascot
394
+ * for logo compositing, find the face zone for a thumbnail crop,
395
+ * extract a product from a marketing shot — anything that needs
396
+ * a precise mask + bbox from natural-language description.
397
+ *
398
+ * Pricing: flat 5 000 mc per call (~$0.005 — Florence-2 is cheap).
399
+ */
400
+ async invokeSegmentation(args) {
401
+ const { endpoints, fetch: fetchImpl } = this.#deps;
402
+ const choice = this.#resolveSigner(args.mandateId);
403
+ // Normalize image input to base64 + content type.
404
+ let imageBase64;
405
+ let imageContentType;
406
+ if ("base64" in args.image) {
407
+ imageBase64 = args.image.base64;
408
+ imageContentType = args.image.contentType;
409
+ }
410
+ else {
411
+ const buf = await args.image.arrayBuffer();
412
+ imageBase64 = arrayBufferToBase64(buf);
413
+ imageContentType = args.image.type || "image/png";
414
+ }
415
+ const url = computeInvokeUrl(endpoints);
416
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
417
+ const params = {
418
+ app_did: this.#deps.appDid,
419
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
420
+ image_base64: imageBase64,
421
+ image_content_type: imageContentType,
422
+ text_input: args.textInput,
423
+ idempotency_key: idempotencyKey,
424
+ };
425
+ return await this.#signAndPost({
426
+ url,
427
+ method: "aithos.compute_invoke_segmentation",
428
+ params,
429
+ choice,
430
+ fetchImpl,
431
+ signal: args.signal,
432
+ });
433
+ }
434
+ /* ---------------------------------------------------------------------- */
435
+ /* Transcription — low-level (advanced) API */
436
+ /* */
437
+ /* Four thin wrappers over the JSON-RPC methods. Use these when you want */
438
+ /* to own the upload + polling loop; otherwise prefer `invokeTranscribe` */
439
+ /* which composes them. All four are isomorphic (sign + POST only). */
440
+ /* ---------------------------------------------------------------------- */
441
+ /**
442
+ * Provision a transcription job and get a pre-signed S3 URL to PUT the
443
+ * audio to. No wallet debit. `mandateId` only selects the delegate
444
+ * signer (it is not part of the wire params for prepare).
445
+ */
446
+ async prepareTranscribe(args) {
447
+ const { endpoints, fetch: fetchImpl } = this.#deps;
448
+ const choice = this.#resolveSigner(args.mandateId);
449
+ const url = computeInvokeUrl(endpoints);
450
+ const params = {
451
+ app_did: this.#deps.appDid,
452
+ content_type: args.contentType,
453
+ };
454
+ if (args.durationSecEstimate !== undefined) {
455
+ params.duration_sec_estimate = args.durationSecEstimate;
456
+ }
457
+ const r = await this.#signAndPost({
458
+ url,
459
+ method: "aithos.compute_transcribe_prepare",
460
+ params,
461
+ choice,
462
+ fetchImpl,
463
+ signal: args.signal,
464
+ });
465
+ return {
466
+ jobId: r.job_id,
467
+ uploadUrl: r.upload_url,
468
+ s3ObjectKey: r.s3_object_key,
469
+ expiresAt: r.expires_at,
470
+ };
471
+ }
472
+ /**
473
+ * Verify the uploaded audio, pre-debit the wallet, and launch the AWS
474
+ * Transcribe job. Returns immediately with `status: "running"`.
475
+ */
476
+ async startTranscribe(args) {
477
+ const { endpoints, fetch: fetchImpl } = this.#deps;
478
+ const choice = this.#resolveSigner(args.mandateId);
479
+ const url = computeInvokeUrl(endpoints);
480
+ const params = {
481
+ app_did: this.#deps.appDid,
482
+ mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
483
+ job_id: args.jobId,
484
+ model: args.model,
485
+ duration_sec: args.durationSec,
486
+ };
487
+ if (args.languageCode !== undefined)
488
+ params.language_code = args.languageCode;
489
+ if (args.diarization !== undefined)
490
+ params.diarization = args.diarization;
491
+ if (args.idempotencyKey !== undefined)
492
+ params.idempotency_key = args.idempotencyKey;
493
+ const r = await this.#signAndPost({
494
+ url,
495
+ method: "aithos.compute_transcribe_start",
496
+ params,
497
+ choice,
498
+ fetchImpl,
499
+ signal: args.signal,
500
+ });
501
+ return {
502
+ jobId: r.job_id,
503
+ status: "running",
504
+ estimatedCredits: r.estimated_credits,
505
+ walletBalance: r.walletBalance,
506
+ ...(r.fundedBy ? { fundedBy: r.fundedBy } : {}),
507
+ ...(r.receiptId ? { receiptId: r.receiptId } : {}),
508
+ };
509
+ }
510
+ /**
511
+ * Poll a job's status. On completion the server finalises (reconcile +
512
+ * audit) and returns the transcript; a resumed poll after reconnect
513
+ * re-reads the transcript while it's still in the 24h output window.
514
+ */
515
+ async getTranscribeStatus(args) {
516
+ const { endpoints, fetch: fetchImpl } = this.#deps;
517
+ const choice = this.#resolveSigner(args.mandateId);
518
+ const url = computeInvokeUrl(endpoints);
519
+ const params = { job_id: args.jobId };
520
+ const r = await this.#signAndPost({
521
+ url,
522
+ method: "aithos.compute_transcribe_status",
523
+ params,
524
+ choice,
525
+ fetchImpl,
526
+ signal: args.signal,
527
+ });
528
+ return mapTranscribeStatus(args.jobId, r);
529
+ }
530
+ /**
531
+ * List the caller's transcription jobs. Excludes terminal `completed`
532
+ * jobs unless `includeCompleted` is set — the resilience "what's still
533
+ * pending server-side" query.
534
+ */
535
+ async listPendingTranscribes(args) {
536
+ const { endpoints, fetch: fetchImpl } = this.#deps;
537
+ const choice = this.#resolveSigner(args?.mandateId);
538
+ const url = computeInvokeUrl(endpoints);
539
+ const params = {};
540
+ if (args?.includeCompleted !== undefined) {
541
+ params.include_completed = args.includeCompleted;
542
+ }
543
+ const r = await this.#signAndPost({
544
+ url,
545
+ method: "aithos.compute_transcribe_list_pending",
546
+ params,
547
+ choice,
548
+ fetchImpl,
549
+ signal: args?.signal,
550
+ });
551
+ return {
552
+ jobs: (r.jobs ?? []).map((j) => ({
553
+ jobId: String(j.job_id),
554
+ status: j.status,
555
+ createdAt: Number(j.created_at),
556
+ ...(j.estimated_credits !== undefined
557
+ ? { estimatedCredits: Number(j.estimated_credits) }
558
+ : {}),
559
+ ...(j.creditsCharged !== undefined
560
+ ? { creditsCharged: Number(j.creditsCharged) }
561
+ : {}),
562
+ })),
563
+ };
564
+ }
565
+ /* ---------------------------------------------------------------------- */
566
+ /* Transcription — high-level one-call API */
567
+ /* ---------------------------------------------------------------------- */
568
+ /**
569
+ * Transcribe an audio Blob to text in one call. Composes the four
570
+ * low-level methods: prepare → direct S3 upload → start → poll. Returns
571
+ * the transcript and stores NOTHING server-side beyond the ephemeral
572
+ * job — the consumer decides what to do with the result.
573
+ *
574
+ * Isomorphic: depends only on `Blob`, `fetch`/`XMLHttpRequest` and
575
+ * timers. On a backend, pass `durationSecOverride` (no Blob duration
576
+ * probing is possible without a DOM); in a browser the duration is
577
+ * probed automatically when omitted.
578
+ *
579
+ * Resilience: the job id is recorded in a localStorage tracker (browser)
580
+ * before upload, so `listLocalPendingTranscribes()` / `resumeTranscribe()`
581
+ * can recover a job whose result never arrived. In Node the tracker is a
582
+ * harmless in-memory no-op.
583
+ */
584
+ async invokeTranscribe(args) {
585
+ const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
586
+ const model = args.model ?? "transcribe:aws-fr-standard";
587
+ const contentType = args.audio.type || "audio/webm";
588
+ const durationSec = args.durationSecOverride ?? (await probeBlobDuration(args.audio));
589
+ args.onProgress?.({ phase: "queued" });
590
+ // 1. Prepare — pre-signed S3 URL + job id.
591
+ const prepared = await this.prepareTranscribe({
592
+ contentType,
593
+ durationSecEstimate: durationSec,
594
+ ...(args.mandateId ? { mandateId: args.mandateId } : {}),
595
+ ...(args.signal ? { signal: args.signal } : {}),
596
+ });
597
+ const tracker = this.#tracker();
598
+ tracker.upsert(prepared.jobId, "uploading", { model, contentType });
599
+ // 2. Upload the blob straight to S3 (the proxy never sees the bytes).
600
+ try {
601
+ await uploadToS3WithProgress(prepared.uploadUrl, args.audio, contentType, {
602
+ onProgress: (b, t) => args.onProgress?.({ phase: "uploading", bytesUploaded: b, totalBytes: t }),
603
+ signal: args.signal,
604
+ fetchImpl: this.#deps.fetch,
605
+ });
606
+ }
607
+ catch (e) {
608
+ tracker.upsert(prepared.jobId, "failed");
609
+ throw e instanceof AithosSDKError
610
+ ? e
611
+ : new AithosSDKError("transcribe_upload_failed", e.message);
612
+ }
613
+ // 3. Start — pre-debit + launch AWS Transcribe.
614
+ args.onProgress?.({ phase: "starting" });
615
+ await this.startTranscribe({
616
+ jobId: prepared.jobId,
617
+ ...(args.mandateId ? { mandateId: args.mandateId } : {}),
618
+ model,
619
+ durationSec,
620
+ ...(args.languageCode ? { languageCode: args.languageCode } : {}),
621
+ ...(args.diarization !== undefined ? { diarization: args.diarization } : {}),
622
+ idempotencyKey,
623
+ ...(args.signal ? { signal: args.signal } : {}),
624
+ });
625
+ tracker.upsert(prepared.jobId, "running");
626
+ // 4. Poll to completion.
627
+ const result = await this.#pollUntilTerminal(prepared.jobId, {
628
+ ...(args.mandateId ? { mandateId: args.mandateId } : {}),
629
+ ...(args.signal ? { signal: args.signal } : {}),
630
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
631
+ ...(args.pollIntervalMs ? { pollIntervalMs: args.pollIntervalMs } : {}),
632
+ });
633
+ tracker.remove(prepared.jobId);
634
+ return result;
635
+ }
636
+ /**
637
+ * Resume polling an in-flight job by id — for recovery after a reload or
638
+ * crash. Returns the final result and clears the job from the local
639
+ * pending tracker. Throws if the job has already failed.
640
+ */
641
+ async resumeTranscribe(jobId, opts) {
642
+ const result = await this.#pollUntilTerminal(jobId, {
643
+ ...(opts?.mandateId ? { mandateId: opts.mandateId } : {}),
644
+ ...(opts?.signal ? { signal: opts.signal } : {}),
645
+ ...(opts?.onProgress ? { onProgress: opts.onProgress } : {}),
646
+ ...(opts?.pollIntervalMs ? { pollIntervalMs: opts.pollIntervalMs } : {}),
647
+ });
648
+ this.#tracker().remove(jobId);
649
+ return result;
650
+ }
651
+ async #pollUntilTerminal(jobId, opts) {
652
+ const startedAt = Date.now();
653
+ let backoffMs = opts.pollIntervalMs ?? 2000;
654
+ for (;;) {
655
+ if (opts.signal?.aborted) {
656
+ throw new AithosSDKError("aborted", "transcription aborted");
657
+ }
658
+ const st = await this.getTranscribeStatus({
659
+ jobId,
660
+ ...(opts.mandateId ? { mandateId: opts.mandateId } : {}),
661
+ ...(opts.signal ? { signal: opts.signal } : {}),
662
+ });
663
+ if (st.status === "completed") {
664
+ opts.onProgress?.({ phase: "completed" });
665
+ return {
666
+ text: st.text,
667
+ segments: st.segments,
668
+ words: st.words,
669
+ durationSec: st.durationSec,
670
+ languageCode: st.languageCode,
671
+ creditsCharged: st.creditsCharged,
672
+ walletBalance: st.walletBalance,
673
+ auditId: st.auditId,
674
+ jobId: st.jobId,
675
+ ...(st.fundedBy ? { fundedBy: st.fundedBy } : {}),
676
+ ...(st.sponsoredBy ? { sponsoredBy: st.sponsoredBy } : {}),
677
+ ...(st.receiptId ? { receiptId: st.receiptId } : {}),
678
+ };
679
+ }
680
+ if (st.status === "failed") {
681
+ this.#tracker().upsert(jobId, "failed");
682
+ throw new AithosSDKError(st.error.code || "transcribe_failed", st.error.message);
683
+ }
684
+ opts.onProgress?.({
685
+ phase: "processing",
686
+ elapsedSec: Math.floor((Date.now() - startedAt) / 1000),
687
+ });
688
+ await sleep(backoffMs, opts.signal);
689
+ backoffMs = Math.min(15_000, Math.ceil(backoffMs * 1.5));
690
+ }
691
+ }
692
+ /* ---------------------------------------------------------------------- */
693
+ /* Transcription — browser resilience (framework-agnostic) */
694
+ /* ---------------------------------------------------------------------- */
695
+ /** Snapshot of locally-tracked in-flight jobs (stable ref between mutations). */
696
+ listLocalPendingTranscribes() {
697
+ return this.#tracker().list();
698
+ }
699
+ /** Stable snapshot for `useSyncExternalStore`-style consumers. */
700
+ getLocalPendingTranscribesSnapshot() {
701
+ return this.#tracker().getSnapshot();
702
+ }
703
+ /**
704
+ * Subscribe to changes in the local pending-jobs registry. Returns an
705
+ * unsubscribe function. Framework-agnostic: wrap it in a React
706
+ * `useSyncExternalStore`, a Vue effect, a Svelte store, etc.
707
+ */
708
+ subscribeLocalPendingTranscribes(listener) {
709
+ return this.#tracker().subscribe(listener);
710
+ }
711
+ /**
712
+ * IndexedDB-backed draft queue: persist a recording before any network
713
+ * call, upload it when the user confirms. Browser-only (methods reject
714
+ * with TranscribeDraftUnavailableError when IndexedDB is absent).
715
+ */
716
+ get transcribeDraft() {
717
+ const store = this.#draft();
718
+ const self = this;
719
+ return {
720
+ save: (blob, meta) => store.save(blob, meta),
721
+ list: () => store.list(),
722
+ get: (draftId) => store.get(draftId),
723
+ delete: (draftId) => store.delete(draftId),
724
+ upload: async (draftId, args) => {
725
+ const rec = await store.get(draftId);
726
+ if (!rec) {
727
+ throw new AithosSDKError("draft_not_found", `no transcription draft '${draftId}'`);
728
+ }
729
+ const result = await self.invokeTranscribe({ ...args, audio: rec.blob });
730
+ await store.delete(draftId);
731
+ return result;
732
+ },
733
+ };
734
+ }
735
+ /**
736
+ * Lazily-built EthosNamespace for the local agent dispatch — reuses the
737
+ * compute deps (auth / endpoints / fetch), so no extra wiring in sdk.ts.
738
+ */
739
+ #ethosNs = null;
740
+ #ethosNamespace() {
741
+ if (!this.#ethosNs) {
742
+ this.#ethosNs = new EthosNamespace({
743
+ auth: this.#deps.auth,
744
+ endpoints: this.#deps.endpoints,
745
+ fetch: this.#deps.fetch,
746
+ });
747
+ }
748
+ return this.#ethosNs;
749
+ }
750
+ /**
751
+ * Resolve the subject DID the local agent operates on:
752
+ * 1. explicit `subjectDid` always wins;
753
+ * 2. else the signed-in owner;
754
+ * 3. else (delegate session) the subject of the imported mandate.
755
+ */
756
+ #resolveSubjectDid(explicit, mandateId) {
757
+ if (explicit && explicit.length > 0)
758
+ return explicit;
759
+ const owner = this.#deps.auth._getOwnerSigners();
760
+ if (owner && !owner.destroyed)
761
+ return owner.did;
762
+ if (mandateId) {
763
+ const actor = this.#deps.auth._getDelegateActor(mandateId);
764
+ if (actor && !actor.destroyed)
765
+ return actor.subjectDid;
766
+ }
767
+ throw new AithosSDKError("sdk_no_signer", "cannot determine the subject DID: sign in as an owner, pass a mandateId for a delegate session, or pass subjectDid explicitly.");
768
+ }
769
+ /** Scopes carried by the delegate mandate for `did` (empty if none). */
770
+ #delegateScopesForSubject(did) {
771
+ const actor = this.#deps.auth._findDelegateForSubject(did);
772
+ const raw = actor?.mandate?.scopes;
773
+ return Array.isArray(raw) ? raw.filter((s) => typeof s === "string") : [];
774
+ }
775
+ /**
776
+ * Resolve the active signer (owner takes precedence over delegate).
777
+ *
778
+ * - When an owner is signed in: returns the owner-signer regardless
779
+ * of `mandateId` (the proxy ignores params.mandate_id on
780
+ * owner-signed envelopes, so owners can omit it).
781
+ * - When delegate-only: requires `mandateId` to find the matching
782
+ * imported bundle. Throws `sdk_no_delegate_for_mandate` if absent
783
+ * or no match.
784
+ */
785
+ #resolveSigner(mandateId) {
786
+ const { auth } = this.#deps;
787
+ const owner = auth._getOwnerSigners();
788
+ const ownerLoaded = owner !== null && !owner.destroyed;
789
+ if (ownerLoaded) {
790
+ const publicKp = ownerKeyPair(owner, "public");
791
+ return {
792
+ kind: "owner",
793
+ iss: owner.did,
794
+ verificationMethod: `${owner.did}#public`,
795
+ signer: publicKp,
796
+ mandate: undefined,
797
+ };
798
+ }
799
+ if (mandateId === undefined || mandateId.length === 0) {
800
+ throw new AithosSDKError("sdk_no_signer", "no owner signed in and no mandateId provided — pass a mandateId for a delegate session, or sign in as an owner first.");
801
+ }
802
+ const actor = auth._getDelegateActor(mandateId);
803
+ if (!actor || actor.destroyed) {
804
+ throw new AithosSDKError("sdk_no_delegate_for_mandate", `no owner signed in and no imported delegate mandate matches '${mandateId}'. Sign in as an owner, or import a delegate bundle for that mandate via auth.importMandate.`);
805
+ }
806
+ const kp = delegateKeyPair(actor);
807
+ return {
808
+ kind: "delegate",
809
+ iss: actor.subjectDid,
810
+ verificationMethod: actor.granteePubkeyMultibase,
811
+ signer: kp,
812
+ // The DelegateActor stores the SignedMandate as a structurally
813
+ // opaque object so the SDK doesn't have to import the
814
+ // protocol-client type at the storage boundary. Round-trip
815
+ // through `unknown` for the TS cast — at runtime the bytes are
816
+ // the canonical SignedMandate the bundle parser already
817
+ // validated.
818
+ mandate: actor.mandate,
819
+ };
820
+ }
821
+ /**
822
+ * Resolve the `mandate_id` value the SDK writes into the JSON-RPC
823
+ * params for the wire. The proxy requires this field to be a
824
+ * non-empty string but only consults it for the delegate path —
825
+ * for owner-signed envelopes it's audit-log metadata, with no
826
+ * security implication.
827
+ *
828
+ * Strategy:
829
+ * - Caller-provided id always wins (owner who wants to attribute
830
+ * to a specific minted mandate can still pass it).
831
+ * - Delegate path: the choice carries the real mandate.id — use it
832
+ * so a delegate cannot accidentally lie about which mandate
833
+ * it claims to spend under.
834
+ * - Owner path with no explicit id: use the owner DID followed
835
+ * by `#self` — a stable sentinel that's unambiguously not a
836
+ * real mandate id (mandate ids are ULIDs).
837
+ */
838
+ #resolveMandateIdForWire(explicit, choice) {
839
+ if (explicit && explicit.length > 0)
840
+ return explicit;
841
+ if (choice.kind === "delegate")
842
+ return choice.mandate.id;
843
+ // Owner-direct sentinel. The proxy never inspects this value
844
+ // beyond non-empty-string validation when no mandate is attached.
845
+ return `${choice.iss}#self`;
846
+ }
847
+ /**
848
+ * Sign the params with the resolved signer and POST a JSON-RPC
849
+ * request. Shared between `invokeBedrock` and `invokeImage` — the
850
+ * only difference between those two is the method name and the
851
+ * params shape; the envelope construction + transport + error
852
+ * mapping is identical.
853
+ */
854
+ async #signAndPost(opts) {
855
+ const { url, method, params, choice, fetchImpl, signal } = opts;
65
856
  const envelope = buildSignedEnvelope({
66
- iss: owner.did,
857
+ iss: choice.iss,
67
858
  aud: url,
68
- method: "aithos.compute_invoke",
69
- verificationMethod: `${owner.did}#public`,
859
+ method,
860
+ verificationMethod: choice.verificationMethod,
70
861
  params,
71
- signer: publicKp,
862
+ signer: choice.signer,
863
+ ...(choice.kind === "delegate" ? { mandate: choice.mandate } : {}),
72
864
  });
73
865
  let res;
74
866
  try {
@@ -77,11 +869,11 @@ export class ComputeNamespace {
77
869
  headers: { "content-type": "application/json" },
78
870
  body: JSON.stringify({
79
871
  jsonrpc: "2.0",
80
- id: "aithos.compute_invoke",
81
- method: "aithos.compute_invoke",
872
+ id: method,
873
+ method,
82
874
  params: { ...params, _envelope: envelope },
83
875
  }),
84
- ...(args.signal ? { signal: args.signal } : {}),
876
+ ...(signal ? { signal } : {}),
85
877
  });
86
878
  }
87
879
  catch (e) {
@@ -109,4 +901,160 @@ function generateIdempotencyKey() {
109
901
  }
110
902
  return hex;
111
903
  }
904
+ /**
905
+ * Probe an audio Blob's duration (seconds) in a browser. Returns 0 when no
906
+ * DOM is available (Node / SSR) or probing fails — the caller should pass
907
+ * `durationSecOverride` on a backend. Never throws.
908
+ */
909
+ async function probeBlobDuration(blob) {
910
+ try {
911
+ if (typeof document === "undefined" ||
912
+ typeof URL === "undefined" ||
913
+ typeof Audio === "undefined") {
914
+ return 0;
915
+ }
916
+ return await new Promise((resolve) => {
917
+ const url = URL.createObjectURL(blob);
918
+ const audio = new Audio();
919
+ let settled = false;
920
+ const done = (v) => {
921
+ if (settled)
922
+ return;
923
+ settled = true;
924
+ try {
925
+ URL.revokeObjectURL(url);
926
+ }
927
+ catch {
928
+ /* ignore */
929
+ }
930
+ resolve(Number.isFinite(v) && v > 0 ? v : 0);
931
+ };
932
+ audio.preload = "metadata";
933
+ audio.onloadedmetadata = () => done(audio.duration);
934
+ audio.onerror = () => done(0);
935
+ audio.src = url;
936
+ setTimeout(() => done(0), 5000);
937
+ });
938
+ }
939
+ catch {
940
+ return 0;
941
+ }
942
+ }
943
+ /**
944
+ * PUT a Blob to a pre-signed S3 URL. Uses XMLHttpRequest in browsers for
945
+ * real upload-progress events; falls back to `fetch` on Node (coarse
946
+ * 0→100% progress). Honors an AbortSignal.
947
+ */
948
+ async function uploadToS3WithProgress(url, blob, contentType, opts) {
949
+ const total = blob.size;
950
+ if (typeof XMLHttpRequest !== "undefined") {
951
+ await new Promise((resolve, reject) => {
952
+ const xhr = new XMLHttpRequest();
953
+ xhr.open("PUT", url, true);
954
+ xhr.setRequestHeader("Content-Type", contentType);
955
+ xhr.upload.onprogress = (e) => {
956
+ if (e.lengthComputable)
957
+ opts.onProgress?.(e.loaded, e.total);
958
+ };
959
+ xhr.onload = () => {
960
+ if (xhr.status >= 200 && xhr.status < 300)
961
+ resolve();
962
+ else
963
+ reject(new AithosSDKError("transcribe_upload_failed", `S3 upload failed: HTTP ${xhr.status}`));
964
+ };
965
+ xhr.onerror = () => reject(new AithosSDKError("transcribe_upload_failed", "S3 upload network error"));
966
+ xhr.onabort = () => reject(new AithosSDKError("aborted", "S3 upload aborted"));
967
+ if (opts.signal) {
968
+ if (opts.signal.aborted) {
969
+ xhr.abort();
970
+ return;
971
+ }
972
+ opts.signal.addEventListener("abort", () => xhr.abort(), { once: true });
973
+ }
974
+ xhr.send(blob);
975
+ });
976
+ return;
977
+ }
978
+ // Node / no XHR — use the injected fetch (falls back to global fetch).
979
+ opts.onProgress?.(0, total);
980
+ const doFetch = opts.fetchImpl ?? fetch;
981
+ const res = await doFetch(url, {
982
+ method: "PUT",
983
+ headers: { "content-type": contentType },
984
+ body: blob,
985
+ ...(opts.signal ? { signal: opts.signal } : {}),
986
+ });
987
+ if (!res.ok) {
988
+ throw new AithosSDKError("transcribe_upload_failed", `S3 upload failed: HTTP ${res.status}`);
989
+ }
990
+ opts.onProgress?.(total, total);
991
+ }
992
+ /** Sleep `ms`, rejecting early if the signal aborts. */
993
+ function sleep(ms, signal) {
994
+ return new Promise((resolve, reject) => {
995
+ const t = setTimeout(resolve, ms);
996
+ if (signal) {
997
+ if (signal.aborted) {
998
+ clearTimeout(t);
999
+ reject(new AithosSDKError("aborted", "aborted"));
1000
+ return;
1001
+ }
1002
+ signal.addEventListener("abort", () => {
1003
+ clearTimeout(t);
1004
+ reject(new AithosSDKError("aborted", "aborted"));
1005
+ }, { once: true });
1006
+ }
1007
+ });
1008
+ }
1009
+ /** Coerce a raw `transcribe_status` JSON-RPC result into the typed union. */
1010
+ function mapTranscribeStatus(jobId, r) {
1011
+ const status = r.status;
1012
+ if (status === "completed") {
1013
+ const fundedBy = r.fundedBy;
1014
+ return {
1015
+ jobId,
1016
+ status: "completed",
1017
+ text: String(r.text ?? ""),
1018
+ segments: r.segments ?? [],
1019
+ words: r.words ?? [],
1020
+ durationSec: Number(r.duration_sec_actual ?? 0),
1021
+ languageCode: String(r.language_code ?? ""),
1022
+ creditsCharged: Number(r.creditsCharged ?? 0),
1023
+ walletBalance: Number(r.walletBalance ?? 0),
1024
+ auditId: String(r.auditId ?? ""),
1025
+ ...(fundedBy === "sponsored" || fundedBy === "grant" || fundedBy === "purchase"
1026
+ ? { fundedBy }
1027
+ : {}),
1028
+ ...(typeof r.sponsoredBy === "string" ? { sponsoredBy: r.sponsoredBy } : {}),
1029
+ ...(typeof r.receiptId === "string" ? { receiptId: r.receiptId } : {}),
1030
+ };
1031
+ }
1032
+ if (status === "failed") {
1033
+ const err = r.error ?? {};
1034
+ return {
1035
+ jobId,
1036
+ status: "failed",
1037
+ error: {
1038
+ code: String(err.code ?? "transcribe_failed"),
1039
+ message: String(err.message ?? ""),
1040
+ },
1041
+ };
1042
+ }
1043
+ return { jobId, status: "running", elapsedSec: Number(r.elapsed_sec ?? 0) };
1044
+ }
1045
+ /**
1046
+ * Encode an ArrayBuffer as base64 in environments where `Buffer` is
1047
+ * not available (browser). Uses btoa over a binary string — safe for
1048
+ * the small payload sizes the SDK deals with (≤ a few MB).
1049
+ */
1050
+ function arrayBufferToBase64(buf) {
1051
+ const bytes = new Uint8Array(buf);
1052
+ let bin = "";
1053
+ // Process in chunks to avoid stack overflow on String.fromCharCode.apply
1054
+ const CHUNK = 0x8000;
1055
+ for (let i = 0; i < bytes.length; i += CHUNK) {
1056
+ bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + CHUNK)));
1057
+ }
1058
+ return btoa(bin);
1059
+ }
112
1060
  //# sourceMappingURL=compute.js.map