@aithos/sdk 0.1.0-alpha.5 → 0.1.0-alpha.51

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