@aithos/sdk 0.1.0-alpha.42 → 0.1.0-alpha.44

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.
@@ -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/ethos.js CHANGED
@@ -485,38 +485,72 @@ export class EthosClient {
485
485
  * Publish height=1 for an owner whose Ethos identity exists on
486
486
  * `api.aithos.be` (provisioned by `auth.signUp()` in alpha.6+) but who
487
487
  * has no editions yet. Builds the manifest from the staged ADD
488
- * mutations on the public zone and POSTs `aithos.publish_ethos_edition`.
488
+ * mutations and POSTs `aithos.publish_ethos_edition`.
489
489
  *
490
- * Limitations of the alpha.7 cut:
491
- * - Public zone only. Staged mutations on circle/self are rejected
492
- * with `ethos_first_edition_public_only` those zones can be
493
- * populated in subsequent editions via the regular
494
- * `publishZoneEdit` path once the public zone has been seeded.
490
+ * Behaviour:
491
+ * - All three zones (public / circle / self) are supported at
492
+ * height=1 since `@aithos/protocol-client@>=0.1.0-alpha.14`. Circle
493
+ * and self sections are sealed via the same DEK + HKDF wrap
494
+ * machinery that the height>=2 path uses, so they're verifiable by
495
+ * the existing reader path with no special-casing.
496
+ * - If the caller staged only circle / self mutations (typical for
497
+ * an app like Linkedone that writes a personality section straight
498
+ * to `circle`), an `aithos-init` sentinel is auto-injected into
499
+ * the public zone. This preserves the invariant that every Ethos
500
+ * has a non-empty public zone at height=1 — which all resolution
501
+ * flows (handle lookup, public crawl) depend on.
495
502
  * - First-edition publishes don't accept update/delete mutations
496
503
  * (there's nothing to update or delete yet) — those are rejected
497
504
  * with `ethos_first_edition_invalid_op`.
505
+ * - The `ethos_first_edition_public_only` error code is no longer
506
+ * emitted; the bucket-and-auto-inject path replaces it.
498
507
  */
499
508
  async #publishFirstEditionOwner() {
500
509
  if (this.#actor.kind !== "owner") {
501
510
  // Defensive — caller already checked this branch.
502
511
  throw new AithosSDKError("ethos_invalid_actor", "expected owner actor");
503
512
  }
504
- // Validate the staged operation set. First edition = ADDs on public
505
- // zone only.
513
+ // Validate the staged operation set and bucket by zone. First
514
+ // edition = ADD mutations only; update / delete are rejected because
515
+ // there's no prior state to mutate.
506
516
  const publicAdds = [];
517
+ const circleAdds = [];
518
+ const selfAdds = [];
507
519
  for (const m of this.#mutations) {
508
520
  if (m.kind !== "add") {
509
521
  throw new AithosSDKError("ethos_first_edition_invalid_op", `first edition: cannot ${m.kind} a section before any edition exists; only addSection is supported on a fresh Ethos`, { data: { mutation: m } });
510
522
  }
511
- if (m.zone !== "public") {
512
- throw new AithosSDKError("ethos_first_edition_public_only", `first edition: only the "public" zone is supported on a fresh Ethos; "${m.zone}" sections can be added after the first publish`, { data: { zone: m.zone } });
513
- }
514
- publicAdds.push({ section: m.section });
515
- }
516
- if (publicAdds.length === 0) {
523
+ if (m.zone === "public")
524
+ publicAdds.push(m.section);
525
+ else if (m.zone === "circle")
526
+ circleAdds.push(m.section);
527
+ else if (m.zone === "self")
528
+ selfAdds.push(m.section);
529
+ }
530
+ if (publicAdds.length === 0 &&
531
+ circleAdds.length === 0 &&
532
+ selfAdds.length === 0) {
517
533
  // Should never reach here — publish() short-circuits on empty
518
534
  // mutations. Belt-and-braces in case the contract drifts.
519
- throw new AithosSDKError("ethos_first_edition_empty", "first edition: stage at least one public-zone section before publishing");
535
+ throw new AithosSDKError("ethos_first_edition_empty", "first edition: stage at least one section before publishing");
536
+ }
537
+ // Public zone invariant: every Ethos has a non-empty public zone at
538
+ // height=1. If the caller only staged encrypted-zone mutations
539
+ // (typical for apps like Linkedone that write straight to circle),
540
+ // auto-inject an `aithos-init` sentinel — identical in shape to
541
+ // what `ensureInitialized()` would produce if called explicitly.
542
+ if (publicAdds.length === 0) {
543
+ publicAdds.push({
544
+ id: "sec_" + randomHex(12),
545
+ title: "aithos-init",
546
+ body: "Ethos initialized.\n\n" +
547
+ "This section is a sentinel created by `EthosClient.publish()` " +
548
+ "to materialise the subject's first edition alongside the " +
549
+ "encrypted-zone content the caller staged. It is safe to delete " +
550
+ "or edit later — its only purpose is to satisfy the invariant " +
551
+ "that every Ethos has a non-empty public zone at height=1.",
552
+ gamma_ref: "gamma_none_" + randomHex(24),
553
+ });
520
554
  }
521
555
  const identity = this.#actor.signers._unsafeStoredIdentity();
522
556
  const browserId = browserIdentityFromStored(identity);
@@ -524,14 +558,27 @@ export class EthosClient {
524
558
  const built = buildSignedFirstEditionFromSections({
525
559
  identity: browserId,
526
560
  signedDidDoc: signedDoc,
527
- publicSections: publicAdds.map((a) => a.section),
561
+ publicSections: publicAdds,
562
+ ...(circleAdds.length > 0 ? { circleSections: circleAdds } : {}),
563
+ ...(selfAdds.length > 0 ? { selfSections: selfAdds } : {}),
528
564
  });
529
565
  const url = writeEndpoint();
566
+ const zonesPayload = {
567
+ public: { bytes_base64: bytesToBase64Padded(built.publicMarkdownBytes) },
568
+ };
569
+ if (built.circleBytes) {
570
+ zonesPayload.circle = {
571
+ bytes_base64: bytesToBase64Padded(built.circleBytes),
572
+ };
573
+ }
574
+ if (built.selfBytes) {
575
+ zonesPayload.self = {
576
+ bytes_base64: bytesToBase64Padded(built.selfBytes),
577
+ };
578
+ }
530
579
  const params = {
531
580
  manifest: built.manifest,
532
- zones: {
533
- public: { bytes_base64: bytesToBase64Padded(built.publicMarkdownBytes) },
534
- },
581
+ zones: zonesPayload,
535
582
  };
536
583
  const envelope = buildSignedEnvelope({
537
584
  iss: browserId.did,
@@ -575,11 +622,16 @@ export class EthosClient {
575
622
  // take the regular next-edition path.
576
623
  this.#ethosHasNoEditionYet = false;
577
624
  this.#afterPublish();
625
+ const zonesPublished = ["public"];
626
+ if (built.circleBytes)
627
+ zonesPublished.push("circle");
628
+ if (built.selfBytes)
629
+ zonesPublished.push("self");
578
630
  return {
579
631
  editionHeight: 1,
580
632
  manifestHash: "", // protocol-client surfaces this on later editions; not on first
581
633
  subjectDid: browserId.did,
582
- zonesPublished: ["public"],
634
+ zonesPublished,
583
635
  };
584
636
  }
585
637
  }
@@ -1,12 +1,14 @@
1
- export declare const VERSION = "0.1.0-alpha.42";
1
+ export declare const VERSION = "0.1.0-alpha.44";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
5
5
  export { AithosRpcError } from "@aithos/protocol-client";
6
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
7
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
8
- export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, } from "./compute.js";
8
+ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, TranscribeModelId, TranscribeProgressState, TranscribeSegment, TranscribeWord, InvokeTranscribeArgs, InvokeTranscribeResult, PrepareTranscribeArgs, PrepareTranscribeResult, StartTranscribeArgs, StartTranscribeResult, TranscribeStatusResult, TranscribeJobSummary, } from "./compute.js";
9
9
  export { ComputeNamespace } from "./compute.js";
10
+ export type { LocalPendingEntry, LocalPendingStatus, TranscribeDraftMeta, TranscribeDraftRecord, } from "./transcribe-resilience.js";
11
+ export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
10
12
  export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
11
13
  export { WalletNamespace } from "./wallet.js";
12
14
  export type { ComponentStyle, ExtractArgs, ExtractContent, ExtractData, ExtractForm, ExtractFormField, ExtractHeading, ExtractIconDeclaration, ExtractImage, ExtractLink, ExtractLogo, ExtractMeta, ExtractResult, ExtractSection, ExtractStructure, ExtractStyles, FetchAssetArgs, FetchAssetResult, PaletteEntry, VisualSignature, WebNamespaceDeps, } from "./web.js";
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
18
18
  // are exported from here. Endpoint config (`AithosSdkEndpoints`,
19
19
  // `DEFAULT_SDK_ENDPOINTS`) likewise.
20
- export const VERSION = "0.1.0-alpha.42";
20
+ export const VERSION = "0.1.0-alpha.44";
21
21
  export { AithosSDK } from "./sdk.js";
22
22
  export { AithosSDKError } from "./types.js";
23
23
  // Re-export protocol-client's JSON-RPC error type so consumers can
@@ -26,6 +26,7 @@ export { AithosSDKError } from "./types.js";
26
26
  export { AithosRpcError } from "@aithos/protocol-client";
27
27
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
28
28
  export { ComputeNamespace } from "./compute.js";
29
+ export { LocalPendingTranscribeTracker, TranscribeDraftStore, TranscribeDraftUnavailableError, } from "./transcribe-resilience.js";
29
30
  export { WalletNamespace } from "./wallet.js";
30
31
  export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
31
32
  // Sign-up, sign-in, sign-in-with-Google. Lives outside the AithosSDK
@@ -25,4 +25,5 @@
25
25
  export { AssetsClientProvider, useAssetsClient, type AssetsClientProviderProps, } from "./context.js";
26
26
  export { useAithosAsset, type UseAithosAssetState, type UseAithosAssetOptions, } from "./use-aithos-asset.js";
27
27
  export { AithosAsset, type AithosAssetProps, type AithosImageProps, type AithosVideoProps, type AithosAudioProps, type AithosDownloadProps, } from "./AithosAsset.js";
28
+ export { useAithosTranscribePendingJobs, type UseAithosTranscribePendingJobs, } from "./use-transcribe-pending.js";
28
29
  //# sourceMappingURL=index.d.ts.map
@@ -27,4 +27,5 @@
27
27
  export { AssetsClientProvider, useAssetsClient, } from "./context.js";
28
28
  export { useAithosAsset, } from "./use-aithos-asset.js";
29
29
  export { AithosAsset, } from "./AithosAsset.js";
30
+ export { useAithosTranscribePendingJobs, } from "./use-transcribe-pending.js";
30
31
  //# sourceMappingURL=index.js.map