@aithos/sdk 0.1.0-alpha.43 → 0.1.0-alpha.45
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/README.md +34 -0
- package/dist/src/compute.d.ts +218 -0
- package/dist/src/compute.js +457 -0
- package/dist/src/data.d.ts +128 -1
- package/dist/src/data.js +159 -15
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.js +3 -2
- package/dist/src/internal/envelope.d.ts +16 -0
- package/dist/src/internal/envelope.js +6 -0
- package/dist/src/mandates.d.ts +22 -1
- package/dist/src/mandates.js +44 -3
- package/dist/src/react/index.d.ts +1 -0
- package/dist/src/react/index.js +1 -0
- package/dist/src/react/use-transcribe-pending.d.ts +21 -0
- package/dist/src/react/use-transcribe-pending.js +47 -0
- package/dist/src/transcribe-resilience.d.ts +57 -0
- package/dist/src/transcribe-resilience.js +203 -0
- package/dist/test/transcribe-invoke.test.d.ts +2 -0
- package/dist/test/transcribe-invoke.test.js +204 -0
- package/dist/test/transcribe.test.d.ts +2 -0
- package/dist/test/transcribe.test.js +186 -0
- package/package.json +1 -1
package/dist/src/compute.js
CHANGED
|
@@ -20,6 +20,7 @@ import { buildSignedEnvelope, } from "@aithos/protocol-client";
|
|
|
20
20
|
import { computeInvokeUrl, } from "./endpoints.js";
|
|
21
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,9 +29,23 @@ 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}.
|
|
@@ -234,6 +249,307 @@ export class ComputeNamespace {
|
|
|
234
249
|
signal: args.signal,
|
|
235
250
|
});
|
|
236
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
|
+
}
|
|
237
553
|
/**
|
|
238
554
|
* Resolve the active signer (owner takes precedence over delegate).
|
|
239
555
|
*
|
|
@@ -363,6 +679,147 @@ function generateIdempotencyKey() {
|
|
|
363
679
|
}
|
|
364
680
|
return hex;
|
|
365
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
|
+
}
|
|
366
823
|
/**
|
|
367
824
|
* Encode an ArrayBuffer as base64 in environments where `Buffer` is
|
|
368
825
|
* not available (browser). Uses btoa over a binary string — safe for
|
package/dist/src/data.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SignedMandate } from "@aithos/protocol-client";
|
|
1
2
|
export interface AithosSchemaLite {
|
|
2
3
|
readonly schema: string;
|
|
3
4
|
readonly indexable: ReadonlySet<string>;
|
|
@@ -45,12 +46,26 @@ export interface CreateDataClientArgs {
|
|
|
45
46
|
export interface DataClient {
|
|
46
47
|
/** Get / create a collection handle. */
|
|
47
48
|
collection(name: string): DataCollection;
|
|
48
|
-
/** Initialize a new collection with an explicit schema.
|
|
49
|
+
/** Initialize a new collection with an explicit schema. Throws
|
|
50
|
+
* `-32073 AITHOS_DATA_COLLECTION_EXISTS` if it already exists. */
|
|
49
51
|
createCollection(args: {
|
|
50
52
|
name: string;
|
|
51
53
|
schema: string;
|
|
52
54
|
forwardSecrecy?: "best_effort" | "strict";
|
|
53
55
|
}): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Get-or-create: create the collection if it doesn't exist, otherwise
|
|
58
|
+
* succeed silently. Idempotent — safe to call on every app boot before
|
|
59
|
+
* writing. Absorbs the `-32073 AITHOS_DATA_COLLECTION_EXISTS` conflict
|
|
60
|
+
* (and the concurrent-create race) so callers don't have to special-case
|
|
61
|
+
* "already there". Avoids the friction where `collection(name).insert(…)`
|
|
62
|
+
* on a never-created collection fails with `-32020`.
|
|
63
|
+
*/
|
|
64
|
+
ensureCollection(args: {
|
|
65
|
+
name: string;
|
|
66
|
+
schema: string;
|
|
67
|
+
forwardSecrecy?: "best_effort" | "strict";
|
|
68
|
+
}): Promise<void>;
|
|
54
69
|
/** List collections owned by this subject. */
|
|
55
70
|
listCollections(): Promise<readonly {
|
|
56
71
|
name: string;
|
|
@@ -102,9 +117,83 @@ export interface DataClient {
|
|
|
102
117
|
getSchema(schemaId: string, opts?: {
|
|
103
118
|
subjectDid?: string;
|
|
104
119
|
}): Promise<object | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Grant a mandate-holding delegate read access to one of this owner's
|
|
122
|
+
* collections, by re-wrapping the collection's CMK to the grantee's
|
|
123
|
+
* key and posting `aithos.data.authorize_app`.
|
|
124
|
+
*
|
|
125
|
+
* Owner-only. The CMK is unwrapped locally (the owner holds it), then
|
|
126
|
+
* re-wrapped X25519-HKDF-AEAD to the grantee's X25519 key (derived
|
|
127
|
+
* from `mandate.grantee.pubkey`). The platform never sees the CMK in
|
|
128
|
+
* clear — it only appends the wrap to the collection's envelope after
|
|
129
|
+
* verifying the mandate (data spec §4.5).
|
|
130
|
+
*
|
|
131
|
+
* Idempotent at the server: re-authorizing the same grantee on the
|
|
132
|
+
* same collection is a no-op. One wrap per grantee covers every record
|
|
133
|
+
* in the collection (O(1) authorization — the CMK is stable).
|
|
134
|
+
*
|
|
135
|
+
* The mandate must carry a `data.<collectionName>.{read|write|admin}`
|
|
136
|
+
* or `data.*.*` scope and a `grantee.pubkey`.
|
|
137
|
+
*/
|
|
138
|
+
authorizeDelegate(args: {
|
|
139
|
+
collectionName: string;
|
|
140
|
+
mandate: SignedMandate;
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Revoke a delegate's access to a collection (`aithos.data.revoke_app`).
|
|
144
|
+
* Owner-only, forward-only: after revocation the PDS refuses the
|
|
145
|
+
* delegate's reads (the mandate is marked revoked), and the delegate's
|
|
146
|
+
* wrap is dropped from the collection's authorization index. Already-read
|
|
147
|
+
* / cached plaintext on the delegate side is out of scope (a known limit
|
|
148
|
+
* of any key-sharing scheme — revocation blocks FUTURE access).
|
|
149
|
+
*/
|
|
150
|
+
revokeDelegate(args: {
|
|
151
|
+
collectionName: string;
|
|
152
|
+
mandateId: string;
|
|
153
|
+
reason?: string;
|
|
154
|
+
}): Promise<void>;
|
|
155
|
+
/** Drop in-memory cache (CMK, collection metadata, …). */
|
|
156
|
+
reset(): void;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Read-only view over a subject's data collections, driven by a mandate
|
|
160
|
+
* the subject granted to a delegate (`data.<collection>.read`). Built by
|
|
161
|
+
* {@link createDelegateDataClient}.
|
|
162
|
+
*
|
|
163
|
+
* Mirror of {@link DataClient} minus every mutating verb: a delegate
|
|
164
|
+
* holding a read mandate can `get`/`list` and enumerate collections, but
|
|
165
|
+
* cannot insert, update, delete, create collections, register schemas, or
|
|
166
|
+
* re-delegate. Those throw `-32042` client-side (and the PDS rejects them
|
|
167
|
+
* server-side regardless).
|
|
168
|
+
*/
|
|
169
|
+
export interface ReadonlyDataClient {
|
|
170
|
+
/** Get a read-only collection handle. */
|
|
171
|
+
collection(name: string): ReadonlyDataCollection;
|
|
172
|
+
/** List collections the delegate's mandate scopes can reach. */
|
|
173
|
+
listCollections(): Promise<readonly {
|
|
174
|
+
name: string;
|
|
175
|
+
schema: string;
|
|
176
|
+
record_count: number;
|
|
177
|
+
}[]>;
|
|
178
|
+
/** List gamma audit entries (read). */
|
|
179
|
+
listGammaEntries(opts?: {
|
|
180
|
+
limit?: number;
|
|
181
|
+
opPrefix?: string;
|
|
182
|
+
verify?: boolean;
|
|
183
|
+
}): Promise<unknown>;
|
|
105
184
|
/** Drop in-memory cache (CMK, collection metadata, …). */
|
|
106
185
|
reset(): void;
|
|
107
186
|
}
|
|
187
|
+
export interface ReadonlyDataCollection {
|
|
188
|
+
readonly name: string;
|
|
189
|
+
/** Fetch one record by id (decrypted client-side via the re-wrapped CMK). */
|
|
190
|
+
get(recordId: string): Promise<Record<string, unknown> | null>;
|
|
191
|
+
/** List records, decrypted. Pagination via opaque cursor. */
|
|
192
|
+
list(opts?: ListOpts): Promise<{
|
|
193
|
+
items: Record<string, unknown>[];
|
|
194
|
+
nextCursor?: string;
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
108
197
|
export interface DataCollection {
|
|
109
198
|
readonly name: string;
|
|
110
199
|
/**
|
|
@@ -150,4 +239,42 @@ export interface ListOpts {
|
|
|
150
239
|
readonly cursor?: string;
|
|
151
240
|
}
|
|
152
241
|
export declare function createDataClient(args: CreateDataClientArgs): DataClient;
|
|
242
|
+
export interface CreateDelegateDataClientArgs {
|
|
243
|
+
/** PDS base URL (same endpoint the owner writes to). */
|
|
244
|
+
readonly pdsUrl: string;
|
|
245
|
+
/** DID of the SUBJECT whose data is being read (the mandate issuer). */
|
|
246
|
+
readonly subjectDid: string;
|
|
247
|
+
/**
|
|
248
|
+
* The full signed mandate the subject granted to this delegate. Must
|
|
249
|
+
* carry a `data.<collection>.read` (or wider) scope and a
|
|
250
|
+
* `grantee.pubkey` matching `delegateSeed`.
|
|
251
|
+
*/
|
|
252
|
+
readonly mandate: SignedMandate;
|
|
253
|
+
/** The delegate's Ed25519 seed (32 bytes) — the grantee key the mandate
|
|
254
|
+
* is bound to. Used to sign envelopes AND to derive the X25519 key that
|
|
255
|
+
* unwraps the re-wrapped CMK. */
|
|
256
|
+
readonly delegateSeed: Uint8Array;
|
|
257
|
+
/**
|
|
258
|
+
* The delegate's Ed25519 public key, multibase-encoded. Defaults to
|
|
259
|
+
* `mandate.grantee.pubkey`. This is the bare verificationMethod the PDS
|
|
260
|
+
* binds the delegate envelope to.
|
|
261
|
+
*/
|
|
262
|
+
readonly granteePubkeyMultibase?: string;
|
|
263
|
+
/** App-defined (vendor) schemas, as for {@link createDataClient}. */
|
|
264
|
+
readonly schemas?: readonly AithosSchemaLite[];
|
|
265
|
+
/** `fetch` override (tests). */
|
|
266
|
+
readonly fetch?: typeof fetch;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build a read-only data client that reads a subject's collections under
|
|
270
|
+
* a mandate (delegate path). The returned {@link ReadonlyDataClient}
|
|
271
|
+
* signs every request as the delegate (bare-multibase verificationMethod
|
|
272
|
+
* + the mandate attached to the envelope), and decrypts records using the
|
|
273
|
+
* CMK the owner re-wrapped for this delegate via
|
|
274
|
+
* {@link DataClient.authorizeDelegate}.
|
|
275
|
+
*
|
|
276
|
+
* Writes are not available on the returned type and throw `-32042` if
|
|
277
|
+
* forced.
|
|
278
|
+
*/
|
|
279
|
+
export declare function createDelegateDataClient(args: CreateDelegateDataClientArgs): ReadonlyDataClient;
|
|
153
280
|
//# sourceMappingURL=data.d.ts.map
|