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