@aithos/sdk 0.1.0-alpha.19 → 0.1.0-alpha.20
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/dist/src/compute.d.ts +155 -1
- package/dist/src/compute.js +76 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/test/compute.test.js +162 -0
- package/dist/test/signup-bootstrap.test.d.ts +2 -0
- package/dist/test/signup-bootstrap.test.js +222 -0
- package/package.json +11 -10
package/dist/src/compute.d.ts
CHANGED
|
@@ -19,8 +19,13 @@ export interface InvokeBedrockArgs {
|
|
|
19
19
|
readonly mandateId?: string;
|
|
20
20
|
/**
|
|
21
21
|
* Model id. Today the proxy accepts the canonical Aithos identifiers:
|
|
22
|
-
* `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-
|
|
22
|
+
* `claude-sonnet-4-6`, `claude-haiku-4-5`, `claude-opus-4-6`.
|
|
23
23
|
* The proxy maps these to Bedrock cross-region inference profiles.
|
|
24
|
+
*
|
|
25
|
+
* NOTE: `claude-opus-4-7` is also provisioned on the Bedrock account
|
|
26
|
+
* but commercial access is gated behind an AWS Sales unlock (as of
|
|
27
|
+
* May 2026) — InvokeModel returns AccessDeniedException pointing to
|
|
28
|
+
* AWS Sales. Stick with `claude-opus-4-6` until the unlock lands.
|
|
24
29
|
*/
|
|
25
30
|
readonly model: string;
|
|
26
31
|
/** Conversation messages (user / assistant turns). */
|
|
@@ -187,6 +192,119 @@ export interface InvokeSegmentationResult {
|
|
|
187
192
|
readonly walletBalance: number;
|
|
188
193
|
readonly auditId: string;
|
|
189
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Models accepted by `invokeUrlFetch`. These are the Anthropic-API-direct
|
|
197
|
+
* model aliases — the proxy resolves them to the canonical API model id
|
|
198
|
+
* (`claude-haiku-4-5-20251001`, etc.) at dispatch time.
|
|
199
|
+
*
|
|
200
|
+
* Bedrock's compute_invoke supports the same names but routes through
|
|
201
|
+
* Bedrock; url_fetch routes through `api.anthropic.com` directly because
|
|
202
|
+
* the `web_fetch` server-side tool isn't exposed via Bedrock.
|
|
203
|
+
*/
|
|
204
|
+
export type UrlFetchModelId = "claude-haiku-4-5" | "claude-sonnet-4-6" | "claude-opus-4-6";
|
|
205
|
+
export interface InvokeUrlFetchArgs {
|
|
206
|
+
/**
|
|
207
|
+
* Mandate ID under which this call should be attributed.
|
|
208
|
+
*
|
|
209
|
+
* - **Owner sessions**: optional. The owner's own DID is used as a
|
|
210
|
+
* sentinel "self" mandate id; the proxy skips all mandate checks
|
|
211
|
+
* for owner-signed envelopes.
|
|
212
|
+
* - **Delegate sessions**: required. Must reference an imported
|
|
213
|
+
* mandate bundle that carries the `compute.url_fetch` scope (NOT
|
|
214
|
+
* the same scope as `compute.invoke` — see the protocol spec).
|
|
215
|
+
*/
|
|
216
|
+
readonly mandateId?: string;
|
|
217
|
+
/**
|
|
218
|
+
* Anthropic model alias. Default `"claude-haiku-4-5"` — fastest and
|
|
219
|
+
* cheapest model that supports `web_fetch`. Bump to Sonnet 4.6 for
|
|
220
|
+
* deeper reasoning over the fetched content; Opus 4.6 for the
|
|
221
|
+
* heaviest analyses (priced accordingly via reconcile). Opus 4.7 is
|
|
222
|
+
* provisioned but commercially gated — see the docstring on
|
|
223
|
+
* InvokeBedrockArgs.model for the unlock path.
|
|
224
|
+
*/
|
|
225
|
+
readonly model?: UrlFetchModelId;
|
|
226
|
+
/**
|
|
227
|
+
* User prompt — should normally contain the URL(s) the caller wants
|
|
228
|
+
* the model to fetch and analyze. Example:
|
|
229
|
+
* "Voici l'URL https://tata.com — résume le service, identifie
|
|
230
|
+
* le style du site et la couleur primaire. Renvoie en JSON."
|
|
231
|
+
*/
|
|
232
|
+
readonly prompt: string;
|
|
233
|
+
/** Optional system prompt (Anthropic-style separate field). */
|
|
234
|
+
readonly system?: string;
|
|
235
|
+
/** Cap on output tokens. Default 2048. */
|
|
236
|
+
readonly maxTokens?: number;
|
|
237
|
+
/** Sampling temperature. Default model-dependent. */
|
|
238
|
+
readonly temperature?: number;
|
|
239
|
+
/**
|
|
240
|
+
* Maximum number of `web_fetch` invocations the model is allowed to
|
|
241
|
+
* make in this call. Default 5; range [1, 10].
|
|
242
|
+
*/
|
|
243
|
+
readonly maxFetches?: number;
|
|
244
|
+
/**
|
|
245
|
+
* Maximum tokens of fetched content Anthropic will inject per fetch.
|
|
246
|
+
* Default 100_000; range [1000, 200_000]. Pages larger than this
|
|
247
|
+
* are truncated server-side.
|
|
248
|
+
*/
|
|
249
|
+
readonly maxContentTokens?: number;
|
|
250
|
+
/**
|
|
251
|
+
* When `true` (default), the response carries citation spans tying
|
|
252
|
+
* generated text back to fetched documents — useful for displaying
|
|
253
|
+
* "selon X, …" footnotes in the UI without post-processing.
|
|
254
|
+
*/
|
|
255
|
+
readonly citations?: boolean;
|
|
256
|
+
/**
|
|
257
|
+
* Optional domain allowlist — if set, the model can only fetch from
|
|
258
|
+
* these domains. Mutually exclusive with `blockedDomains`.
|
|
259
|
+
*/
|
|
260
|
+
readonly allowedDomains?: readonly string[];
|
|
261
|
+
/**
|
|
262
|
+
* Optional domain blocklist. Mutually exclusive with `allowedDomains`.
|
|
263
|
+
*/
|
|
264
|
+
readonly blockedDomains?: readonly string[];
|
|
265
|
+
/** Idempotency key for retries (generated if omitted). */
|
|
266
|
+
readonly idempotencyKey?: string;
|
|
267
|
+
/** Abort signal to cancel the request. */
|
|
268
|
+
readonly signal?: AbortSignal;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Single citation projection — flattened from Anthropic's wire shape so
|
|
272
|
+
* consumers can render `{cited_text}` next to `{url}` without parsing
|
|
273
|
+
* vendor-specific block types.
|
|
274
|
+
*/
|
|
275
|
+
export interface UrlFetchCitation {
|
|
276
|
+
readonly url: string;
|
|
277
|
+
readonly citedText: string;
|
|
278
|
+
readonly documentTitle?: string;
|
|
279
|
+
readonly startCharIndex?: number;
|
|
280
|
+
readonly endCharIndex?: number;
|
|
281
|
+
}
|
|
282
|
+
/** Per-fetch metadata in the order the model performed the fetches. */
|
|
283
|
+
export interface UrlFetchMetadata {
|
|
284
|
+
readonly url: string;
|
|
285
|
+
readonly retrievedAt?: string;
|
|
286
|
+
readonly title?: string;
|
|
287
|
+
}
|
|
288
|
+
export interface InvokeUrlFetchResult {
|
|
289
|
+
/** Final assistant text — typically a JSON string when the prompt asked for structure. */
|
|
290
|
+
readonly content: string;
|
|
291
|
+
/** Citation spans (empty when `citations: false` or no fetches succeeded). */
|
|
292
|
+
readonly citations: readonly UrlFetchCitation[];
|
|
293
|
+
/** Per-URL fetch metadata. */
|
|
294
|
+
readonly urlsFetched: readonly UrlFetchMetadata[];
|
|
295
|
+
readonly stopReason: "end_turn" | "max_tokens" | "tool_use" | "stop_sequence" | "pause_turn" | "refusal";
|
|
296
|
+
readonly usage: {
|
|
297
|
+
readonly inputTokens: number;
|
|
298
|
+
readonly outputTokens: number;
|
|
299
|
+
readonly webFetchInvocations: number;
|
|
300
|
+
};
|
|
301
|
+
/** Microcredits charged after reconcile (already net of any refund). */
|
|
302
|
+
readonly creditsCharged: number;
|
|
303
|
+
/** Wallet balance after the debit + reconcile. */
|
|
304
|
+
readonly walletBalance: number;
|
|
305
|
+
/** Audit log id for traceability. */
|
|
306
|
+
readonly auditId: string;
|
|
307
|
+
}
|
|
190
308
|
export interface ComputeNamespaceDeps {
|
|
191
309
|
readonly auth: AithosAuth;
|
|
192
310
|
readonly appDid: string;
|
|
@@ -274,5 +392,41 @@ export declare class ComputeNamespace {
|
|
|
274
392
|
* Pricing: flat 5 000 mc per call (~$0.005 — Florence-2 is cheap).
|
|
275
393
|
*/
|
|
276
394
|
invokeSegmentation(args: InvokeSegmentationArgs): Promise<InvokeSegmentationResult>;
|
|
395
|
+
/**
|
|
396
|
+
* Fetch one or more URLs and have Claude analyze the content. Routes
|
|
397
|
+
* through `api.anthropic.com` with the `web_fetch` server-side tool —
|
|
398
|
+
* NOT through Bedrock — because Bedrock does not expose Anthropic's
|
|
399
|
+
* server-side tools (web_fetch, web_search, etc.). The proxy hides
|
|
400
|
+
* this multi-backend detail from the SDK consumer; the wallet,
|
|
401
|
+
* envelope, and mandate-scope contracts are unchanged.
|
|
402
|
+
*
|
|
403
|
+
* Typical use:
|
|
404
|
+
* ```ts
|
|
405
|
+
* const r = await sdk.compute.invokeUrlFetch({
|
|
406
|
+
* prompt: "Voici l'URL https://tata.com — résume le service, " +
|
|
407
|
+
* "identifie le style et la couleur primaire. JSON.",
|
|
408
|
+
* });
|
|
409
|
+
* console.log(r.content);
|
|
410
|
+
* for (const c of r.citations) console.log(c.url, "→", c.citedText);
|
|
411
|
+
* ```
|
|
412
|
+
*
|
|
413
|
+
* Mandate scope: requires `compute.url_fetch` (distinct from
|
|
414
|
+
* `compute.invoke` — see {@link InvokeUrlFetchArgs.mandateId}).
|
|
415
|
+
*
|
|
416
|
+
* Pricing: same Claude 4.x rates as Bedrock (Anthropic's direct API
|
|
417
|
+
* and Bedrock list prices are identical). Default Haiku 4.5 ≈
|
|
418
|
+
* $0.001/1k input + $0.005/1k output. The proxy pre-debits a
|
|
419
|
+
* conservative upper bound (`maxFetches × maxContentTokens × input
|
|
420
|
+
* rate + maxTokens × output rate`) and reconciles down to the actual
|
|
421
|
+
* usage Anthropic reports — so the wallet is always charged the
|
|
422
|
+
* exact post-call cost.
|
|
423
|
+
*
|
|
424
|
+
* @throws {AithosSDKError} on protocol errors. Notable codes:
|
|
425
|
+
* `sdk_no_signer`, `sdk_no_delegate_for_mandate`, `network`,
|
|
426
|
+
* `http`, `empty`, plus proxy codes `-32042` (mandate scope
|
|
427
|
+
* missing), `-32070`/`-32071` (wallet), `-32074` (fetch blocked
|
|
428
|
+
* by robots.txt / domain filter), `-32050` (rate limit).
|
|
429
|
+
*/
|
|
430
|
+
invokeUrlFetch(args: InvokeUrlFetchArgs): Promise<InvokeUrlFetchResult>;
|
|
277
431
|
}
|
|
278
432
|
//# sourceMappingURL=compute.d.ts.map
|
package/dist/src/compute.js
CHANGED
|
@@ -234,6 +234,82 @@ export class ComputeNamespace {
|
|
|
234
234
|
signal: args.signal,
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Fetch one or more URLs and have Claude analyze the content. Routes
|
|
239
|
+
* through `api.anthropic.com` with the `web_fetch` server-side tool —
|
|
240
|
+
* NOT through Bedrock — because Bedrock does not expose Anthropic's
|
|
241
|
+
* server-side tools (web_fetch, web_search, etc.). The proxy hides
|
|
242
|
+
* this multi-backend detail from the SDK consumer; the wallet,
|
|
243
|
+
* envelope, and mandate-scope contracts are unchanged.
|
|
244
|
+
*
|
|
245
|
+
* Typical use:
|
|
246
|
+
* ```ts
|
|
247
|
+
* const r = await sdk.compute.invokeUrlFetch({
|
|
248
|
+
* prompt: "Voici l'URL https://tata.com — résume le service, " +
|
|
249
|
+
* "identifie le style et la couleur primaire. JSON.",
|
|
250
|
+
* });
|
|
251
|
+
* console.log(r.content);
|
|
252
|
+
* for (const c of r.citations) console.log(c.url, "→", c.citedText);
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* Mandate scope: requires `compute.url_fetch` (distinct from
|
|
256
|
+
* `compute.invoke` — see {@link InvokeUrlFetchArgs.mandateId}).
|
|
257
|
+
*
|
|
258
|
+
* Pricing: same Claude 4.x rates as Bedrock (Anthropic's direct API
|
|
259
|
+
* and Bedrock list prices are identical). Default Haiku 4.5 ≈
|
|
260
|
+
* $0.001/1k input + $0.005/1k output. The proxy pre-debits a
|
|
261
|
+
* conservative upper bound (`maxFetches × maxContentTokens × input
|
|
262
|
+
* rate + maxTokens × output rate`) and reconciles down to the actual
|
|
263
|
+
* usage Anthropic reports — so the wallet is always charged the
|
|
264
|
+
* exact post-call cost.
|
|
265
|
+
*
|
|
266
|
+
* @throws {AithosSDKError} on protocol errors. Notable codes:
|
|
267
|
+
* `sdk_no_signer`, `sdk_no_delegate_for_mandate`, `network`,
|
|
268
|
+
* `http`, `empty`, plus proxy codes `-32042` (mandate scope
|
|
269
|
+
* missing), `-32070`/`-32071` (wallet), `-32074` (fetch blocked
|
|
270
|
+
* by robots.txt / domain filter), `-32050` (rate limit).
|
|
271
|
+
*/
|
|
272
|
+
async invokeUrlFetch(args) {
|
|
273
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
274
|
+
const choice = this.#resolveSigner(args.mandateId);
|
|
275
|
+
const url = computeInvokeUrl(endpoints);
|
|
276
|
+
const idempotencyKey = args.idempotencyKey ?? generateIdempotencyKey();
|
|
277
|
+
const model = args.model ?? "claude-haiku-4-5";
|
|
278
|
+
const params = {
|
|
279
|
+
app_did: this.#deps.appDid,
|
|
280
|
+
mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
|
|
281
|
+
model,
|
|
282
|
+
prompt: args.prompt,
|
|
283
|
+
idempotency_key: idempotencyKey,
|
|
284
|
+
};
|
|
285
|
+
if (args.system !== undefined)
|
|
286
|
+
params.system = args.system;
|
|
287
|
+
if (args.maxTokens !== undefined)
|
|
288
|
+
params.max_tokens = args.maxTokens;
|
|
289
|
+
if (args.temperature !== undefined)
|
|
290
|
+
params.temperature = args.temperature;
|
|
291
|
+
if (args.maxFetches !== undefined)
|
|
292
|
+
params.max_fetches = args.maxFetches;
|
|
293
|
+
if (args.maxContentTokens !== undefined) {
|
|
294
|
+
params.max_content_tokens = args.maxContentTokens;
|
|
295
|
+
}
|
|
296
|
+
if (args.citations !== undefined)
|
|
297
|
+
params.citations = args.citations;
|
|
298
|
+
if (args.allowedDomains !== undefined && args.allowedDomains.length > 0) {
|
|
299
|
+
params.allowed_domains = args.allowedDomains;
|
|
300
|
+
}
|
|
301
|
+
if (args.blockedDomains !== undefined && args.blockedDomains.length > 0) {
|
|
302
|
+
params.blocked_domains = args.blockedDomains;
|
|
303
|
+
}
|
|
304
|
+
return await this.#signAndPost({
|
|
305
|
+
url,
|
|
306
|
+
method: "aithos.compute_invoke_url_fetch",
|
|
307
|
+
params,
|
|
308
|
+
choice,
|
|
309
|
+
fetchImpl,
|
|
310
|
+
signal: args.signal,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
237
313
|
/**
|
|
238
314
|
* Resolve the active signer (owner takes precedence over delegate).
|
|
239
315
|
*
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ 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, InvokeUrlFetchArgs, InvokeUrlFetchResult, SegmentPolygon, StopReason, UrlFetchCitation, UrlFetchMetadata, UrlFetchModelId, } from "./compute.js";
|
|
9
9
|
export { ComputeNamespace } from "./compute.js";
|
|
10
10
|
export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
|
|
11
11
|
export { WalletNamespace } from "./wallet.js";
|
|
@@ -187,4 +187,166 @@ describe("compute.invokeBedrock — abort", () => {
|
|
|
187
187
|
assert.equal(receivedSignal, ac.signal);
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
|
+
/* -------------------------------------------------------------------------- */
|
|
191
|
+
/* invokeUrlFetch */
|
|
192
|
+
/* -------------------------------------------------------------------------- */
|
|
193
|
+
const URL_FETCH_HAPPY_RESULT = {
|
|
194
|
+
content: "Tata.com propose un service Y avec une couleur primaire bleu (#1E40AF).",
|
|
195
|
+
citations: [
|
|
196
|
+
{
|
|
197
|
+
url: "https://tata.com",
|
|
198
|
+
citedText: "Notre service révolutionne...",
|
|
199
|
+
documentTitle: "Tata — Home",
|
|
200
|
+
startCharIndex: 0,
|
|
201
|
+
endCharIndex: 28,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
urlsFetched: [
|
|
205
|
+
{
|
|
206
|
+
url: "https://tata.com",
|
|
207
|
+
retrievedAt: "2026-05-12T10:00:00Z",
|
|
208
|
+
title: "Tata — Home",
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
stopReason: "end_turn",
|
|
212
|
+
usage: { inputTokens: 28_500, outputTokens: 420, webFetchInvocations: 1 },
|
|
213
|
+
creditsCharged: 35,
|
|
214
|
+
walletBalance: 99_965,
|
|
215
|
+
auditId: "audit-url-1",
|
|
216
|
+
};
|
|
217
|
+
describe("compute.invokeUrlFetch — happy path", () => {
|
|
218
|
+
it("posts to ${compute}/v1/invoke with method=aithos.compute_invoke_url_fetch", async () => {
|
|
219
|
+
let capturedUrl;
|
|
220
|
+
let capturedInit;
|
|
221
|
+
const fakeFetch = async (input, init) => {
|
|
222
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
223
|
+
capturedInit = init;
|
|
224
|
+
return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
|
|
225
|
+
status: 200,
|
|
226
|
+
headers: { "content-type": "application/json" },
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
const sdk = await makeSdk(fakeFetch);
|
|
230
|
+
const out = await sdk.compute.invokeUrlFetch({
|
|
231
|
+
mandateId: "mandate:abc",
|
|
232
|
+
prompt: "Voici l'URL https://tata.com — résume.",
|
|
233
|
+
});
|
|
234
|
+
assert.deepEqual(out, URL_FETCH_HAPPY_RESULT);
|
|
235
|
+
assert.equal(capturedUrl, "https://compute.example.test/v1/invoke");
|
|
236
|
+
const body = JSON.parse(capturedInit?.body);
|
|
237
|
+
assert.equal(body.jsonrpc, "2.0");
|
|
238
|
+
assert.equal(body.method, "aithos.compute_invoke_url_fetch");
|
|
239
|
+
assert.equal(body.params.app_did, APP_DID);
|
|
240
|
+
assert.equal(body.params.mandate_id, "mandate:abc");
|
|
241
|
+
// Default model is Haiku 4.5 (cheapest model that supports web_fetch).
|
|
242
|
+
assert.equal(body.params.model, "claude-haiku-4-5");
|
|
243
|
+
assert.equal(body.params.prompt, "Voici l'URL https://tata.com — résume.");
|
|
244
|
+
// Auto-generated idempotency key.
|
|
245
|
+
assert.match(body.params.idempotency_key, /^[0-9a-f]{32}$/);
|
|
246
|
+
// Envelope is present on the wire.
|
|
247
|
+
assert.ok(body.params._envelope, "request must carry a signed envelope");
|
|
248
|
+
// Optional params NOT forwarded when not provided.
|
|
249
|
+
assert.equal(body.params.system, undefined);
|
|
250
|
+
assert.equal(body.params.max_fetches, undefined);
|
|
251
|
+
assert.equal(body.params.max_content_tokens, undefined);
|
|
252
|
+
assert.equal(body.params.citations, undefined);
|
|
253
|
+
assert.equal(body.params.allowed_domains, undefined);
|
|
254
|
+
assert.equal(body.params.blocked_domains, undefined);
|
|
255
|
+
});
|
|
256
|
+
it("forwards all optional knobs: system / maxTokens / maxFetches / maxContentTokens / citations / allowedDomains / idempotencyKey / model", async () => {
|
|
257
|
+
let capturedBody;
|
|
258
|
+
const fakeFetch = async (_input, init) => {
|
|
259
|
+
capturedBody = JSON.parse(init?.body).params;
|
|
260
|
+
return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
|
|
261
|
+
status: 200,
|
|
262
|
+
headers: { "content-type": "application/json" },
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
const sdk = await makeSdk(fakeFetch);
|
|
266
|
+
await sdk.compute.invokeUrlFetch({
|
|
267
|
+
mandateId: "mandate:abc",
|
|
268
|
+
model: "claude-sonnet-4-6",
|
|
269
|
+
prompt: "Analyse https://tata.com",
|
|
270
|
+
system: "You are a brand analyst.",
|
|
271
|
+
maxTokens: 1024,
|
|
272
|
+
temperature: 0.1,
|
|
273
|
+
maxFetches: 3,
|
|
274
|
+
maxContentTokens: 50_000,
|
|
275
|
+
citations: false,
|
|
276
|
+
allowedDomains: ["tata.com", "*.tata.com"],
|
|
277
|
+
idempotencyKey: "idem-url-1",
|
|
278
|
+
});
|
|
279
|
+
assert.equal(capturedBody?.model, "claude-sonnet-4-6");
|
|
280
|
+
assert.equal(capturedBody?.system, "You are a brand analyst.");
|
|
281
|
+
assert.equal(capturedBody?.max_tokens, 1024);
|
|
282
|
+
assert.equal(capturedBody?.temperature, 0.1);
|
|
283
|
+
assert.equal(capturedBody?.max_fetches, 3);
|
|
284
|
+
assert.equal(capturedBody?.max_content_tokens, 50_000);
|
|
285
|
+
assert.equal(capturedBody?.citations, false);
|
|
286
|
+
assert.deepEqual(capturedBody?.allowed_domains, ["tata.com", "*.tata.com"]);
|
|
287
|
+
assert.equal(capturedBody?.idempotency_key, "idem-url-1");
|
|
288
|
+
// Empty / undefined arrays must NOT bleed through as `[]` on the wire.
|
|
289
|
+
assert.equal(capturedBody?.blocked_domains, undefined);
|
|
290
|
+
});
|
|
291
|
+
it("omits empty allowedDomains / blockedDomains arrays from the wire payload", async () => {
|
|
292
|
+
let capturedBody;
|
|
293
|
+
const fakeFetch = async (_input, init) => {
|
|
294
|
+
capturedBody = JSON.parse(init?.body).params;
|
|
295
|
+
return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
|
|
296
|
+
status: 200,
|
|
297
|
+
headers: { "content-type": "application/json" },
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
const sdk = await makeSdk(fakeFetch);
|
|
301
|
+
await sdk.compute.invokeUrlFetch({
|
|
302
|
+
mandateId: "mandate:abc",
|
|
303
|
+
prompt: "Analyse https://tata.com",
|
|
304
|
+
allowedDomains: [],
|
|
305
|
+
blockedDomains: [],
|
|
306
|
+
});
|
|
307
|
+
assert.equal(capturedBody?.allowed_domains, undefined);
|
|
308
|
+
assert.equal(capturedBody?.blocked_domains, undefined);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
describe("compute.invokeUrlFetch — errors", () => {
|
|
312
|
+
it("wraps a JSON-RPC error from the proxy as AithosSDKError with the proxy code", async () => {
|
|
313
|
+
const fakeFetch = async () => new Response(JSON.stringify({
|
|
314
|
+
error: {
|
|
315
|
+
code: -32074,
|
|
316
|
+
message: "web_fetch failed: robots.txt disallows /api/* — fetch refused",
|
|
317
|
+
data: { detail: "robots_blocked" },
|
|
318
|
+
},
|
|
319
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
320
|
+
const sdk = await makeSdk(fakeFetch);
|
|
321
|
+
await assert.rejects(sdk.compute.invokeUrlFetch({
|
|
322
|
+
mandateId: "mandate:abc",
|
|
323
|
+
prompt: "Analyse https://tata.com/api/secret",
|
|
324
|
+
}), (err) => {
|
|
325
|
+
assert.ok(err instanceof AithosSDKError);
|
|
326
|
+
assert.equal(err.code, "-32074");
|
|
327
|
+
assert.match(err.message, /robots\.txt/);
|
|
328
|
+
return true;
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
describe("compute.invokeUrlFetch — abort", () => {
|
|
333
|
+
it("propagates an AbortSignal to fetch", async () => {
|
|
334
|
+
let receivedSignal;
|
|
335
|
+
const fakeFetch = async (_input, init) => {
|
|
336
|
+
receivedSignal = init?.signal;
|
|
337
|
+
return new Response(JSON.stringify({ result: URL_FETCH_HAPPY_RESULT }), {
|
|
338
|
+
status: 200,
|
|
339
|
+
headers: { "content-type": "application/json" },
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
const sdk = await makeSdk(fakeFetch);
|
|
343
|
+
const ac = new AbortController();
|
|
344
|
+
await sdk.compute.invokeUrlFetch({
|
|
345
|
+
mandateId: "mandate:abc",
|
|
346
|
+
prompt: "Analyse https://tata.com",
|
|
347
|
+
signal: ac.signal,
|
|
348
|
+
});
|
|
349
|
+
assert.equal(receivedSignal, ac.signal);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
190
352
|
//# sourceMappingURL=compute.test.js.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Tests for the Ethos bootstrap step inside AithosAuth.signUp().
|
|
4
|
+
//
|
|
5
|
+
// The contract:
|
|
6
|
+
// 1. POST /auth/register → creates the auth user (auth.aithos.be).
|
|
7
|
+
// 2. POST /mcp/primitives/write with method=aithos.publish_identity →
|
|
8
|
+
// provisions the user's Ethos on api.aithos.be.
|
|
9
|
+
// 3. Hydrate local state ONLY after both steps succeed.
|
|
10
|
+
//
|
|
11
|
+
// Failure modes:
|
|
12
|
+
// - register fails → throw, no publish_identity attempted, no hydrate.
|
|
13
|
+
// - publish_identity rejected (JSON-RPC error) → throw immediately,
|
|
14
|
+
// no retry, no hydrate.
|
|
15
|
+
// - publish_identity 5xx / network error → 2 retries with backoff,
|
|
16
|
+
// then throw `ethos_bootstrap_failed` if all fail.
|
|
17
|
+
//
|
|
18
|
+
// We mock fetch end-to-end so the tests run offline. The protocol-client
|
|
19
|
+
// crypto runs for real — we want to assert that the envelope shape coming
|
|
20
|
+
// out of the SDK matches what api.aithos.be will accept.
|
|
21
|
+
import { strict as assert } from "node:assert";
|
|
22
|
+
import { describe, it } from "node:test";
|
|
23
|
+
import { AithosAuth, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
24
|
+
function makeMockFetch(handlers) {
|
|
25
|
+
const calls = [];
|
|
26
|
+
const fetchImpl = (async (input, init) => {
|
|
27
|
+
const url = String(input);
|
|
28
|
+
const method = init?.method ?? "GET";
|
|
29
|
+
const bodyText = typeof init?.body === "string"
|
|
30
|
+
? init.body
|
|
31
|
+
: init?.body == null
|
|
32
|
+
? null
|
|
33
|
+
: String(init.body);
|
|
34
|
+
const body = bodyText ? JSON.parse(bodyText) : null;
|
|
35
|
+
const call = { url, method, body };
|
|
36
|
+
calls.push(call);
|
|
37
|
+
for (const h of handlers) {
|
|
38
|
+
if (!url.includes(h.url))
|
|
39
|
+
continue;
|
|
40
|
+
if (h.method && h.method !== method)
|
|
41
|
+
continue;
|
|
42
|
+
if (h.remaining !== undefined && h.remaining <= 0)
|
|
43
|
+
continue;
|
|
44
|
+
if (h.remaining !== undefined)
|
|
45
|
+
h.remaining--;
|
|
46
|
+
const out = await h.respond(call);
|
|
47
|
+
const status = out.status ?? 200;
|
|
48
|
+
return new Response(JSON.stringify(out.json), {
|
|
49
|
+
status,
|
|
50
|
+
headers: { "content-type": "application/json" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`unhandled fetch: ${method} ${url}`);
|
|
54
|
+
});
|
|
55
|
+
return { fetch: fetchImpl, calls };
|
|
56
|
+
}
|
|
57
|
+
function fakeRegisterOk() {
|
|
58
|
+
return {
|
|
59
|
+
json: {
|
|
60
|
+
session: "jwt-token-here",
|
|
61
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function fakePublishIdentityOk() {
|
|
66
|
+
return {
|
|
67
|
+
json: {
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
id: "publish_identity",
|
|
70
|
+
result: { ok: true, did_document_url: "https://cdn.aithos.be/ethos/zABC/did.json" },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function makeAuth(fetchImpl) {
|
|
75
|
+
return new AithosAuth({
|
|
76
|
+
authBaseUrl: "https://auth.test",
|
|
77
|
+
apiBaseUrl: "https://api.test",
|
|
78
|
+
fetch: fetchImpl,
|
|
79
|
+
sessionStore: noopStore(),
|
|
80
|
+
keyStore: memoryKeyStore(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const validInput = {
|
|
84
|
+
email: "alice@test.example",
|
|
85
|
+
password: "correct horse battery staple",
|
|
86
|
+
handle: "alice",
|
|
87
|
+
};
|
|
88
|
+
/* -------------------------------------------------------------------------- */
|
|
89
|
+
/* Happy path */
|
|
90
|
+
/* -------------------------------------------------------------------------- */
|
|
91
|
+
describe("AithosAuth.signUp — Ethos bootstrap", () => {
|
|
92
|
+
it("calls /auth/register THEN /mcp/primitives/write with publish_identity", async () => {
|
|
93
|
+
const { fetch: f, calls } = makeMockFetch([
|
|
94
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
95
|
+
{
|
|
96
|
+
url: "/mcp/primitives/write",
|
|
97
|
+
method: "POST",
|
|
98
|
+
respond: fakePublishIdentityOk,
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
const auth = makeAuth(f);
|
|
102
|
+
const r = await auth.signUp(validInput);
|
|
103
|
+
assert.equal(calls.length, 2, "should make exactly 2 calls");
|
|
104
|
+
assert.match(calls[0].url, /\/auth\/register$/);
|
|
105
|
+
assert.match(calls[1].url, /\/mcp\/primitives\/write$/);
|
|
106
|
+
assert.ok(r.session.session === "jwt-token-here");
|
|
107
|
+
assert.ok(auth.canSignAsOwner(), "must be hydrated as owner");
|
|
108
|
+
});
|
|
109
|
+
it("envelope is JSON-RPC publish_identity, signed by #root, with valid params", async () => {
|
|
110
|
+
let publishBody = null;
|
|
111
|
+
const { fetch: f } = makeMockFetch([
|
|
112
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
113
|
+
{
|
|
114
|
+
url: "/mcp/primitives/write",
|
|
115
|
+
method: "POST",
|
|
116
|
+
respond: (call) => {
|
|
117
|
+
publishBody = call.body;
|
|
118
|
+
return fakePublishIdentityOk();
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
await makeAuth(f).signUp(validInput);
|
|
123
|
+
// JSON-RPC envelope shape
|
|
124
|
+
assert.equal(publishBody.jsonrpc, "2.0");
|
|
125
|
+
assert.equal(publishBody.method, "aithos.publish_identity");
|
|
126
|
+
// params include the inline _envelope and the publish_identity payload
|
|
127
|
+
const params = publishBody.params;
|
|
128
|
+
assert.equal(typeof params.handle, "string");
|
|
129
|
+
assert.equal(params.handle, "alice");
|
|
130
|
+
assert.equal(typeof params.display_name, "string");
|
|
131
|
+
assert.ok(params.did_document, "did_document must be present");
|
|
132
|
+
assert.ok(params._envelope, "_envelope must be present");
|
|
133
|
+
// envelope is signed by #root
|
|
134
|
+
const env = params._envelope;
|
|
135
|
+
assert.equal(env["aithos-envelope"], "0.1.0");
|
|
136
|
+
assert.match(env.iss, /^did:aithos:/);
|
|
137
|
+
assert.equal(env.method, "aithos.publish_identity");
|
|
138
|
+
assert.equal(env.aud, "https://api.test/mcp/primitives/write");
|
|
139
|
+
assert.equal(env.proof.type, "Ed25519Signature2020");
|
|
140
|
+
assert.match(env.proof.verificationMethod, /#root$/);
|
|
141
|
+
assert.equal(typeof env.proof.proofValue, "string");
|
|
142
|
+
assert.ok(env.proof.proofValue.length > 0);
|
|
143
|
+
});
|
|
144
|
+
it("does NOT hydrate state when /auth/register fails", async () => {
|
|
145
|
+
const { fetch: f, calls } = makeMockFetch([
|
|
146
|
+
{
|
|
147
|
+
url: "/auth/register",
|
|
148
|
+
method: "POST",
|
|
149
|
+
respond: () => ({
|
|
150
|
+
status: 409,
|
|
151
|
+
json: { error: "email_taken" },
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
]);
|
|
155
|
+
const auth = makeAuth(f);
|
|
156
|
+
await assert.rejects(() => auth.signUp(validInput), AithosSDKError);
|
|
157
|
+
assert.equal(calls.length, 1, "publish_identity must NOT be called");
|
|
158
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
159
|
+
});
|
|
160
|
+
it("does NOT hydrate state when publish_identity returns a JSON-RPC error", async () => {
|
|
161
|
+
const { fetch: f, calls } = makeMockFetch([
|
|
162
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
163
|
+
{
|
|
164
|
+
url: "/mcp/primitives/write",
|
|
165
|
+
method: "POST",
|
|
166
|
+
respond: () => ({
|
|
167
|
+
json: {
|
|
168
|
+
jsonrpc: "2.0",
|
|
169
|
+
id: "publish_identity",
|
|
170
|
+
error: { code: -32600, message: "invalid envelope signature" },
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
175
|
+
const auth = makeAuth(f);
|
|
176
|
+
await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
|
|
177
|
+
// No retry on JSON-RPC error: 1 register + 1 publish.
|
|
178
|
+
assert.equal(calls.length, 2);
|
|
179
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
180
|
+
});
|
|
181
|
+
it("retries publish_identity on 5xx, then succeeds", async () => {
|
|
182
|
+
let publishCalls = 0;
|
|
183
|
+
const { fetch: f } = makeMockFetch([
|
|
184
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
185
|
+
{
|
|
186
|
+
url: "/mcp/primitives/write",
|
|
187
|
+
method: "POST",
|
|
188
|
+
respond: () => {
|
|
189
|
+
publishCalls++;
|
|
190
|
+
if (publishCalls < 2) {
|
|
191
|
+
return { status: 503, json: { error: "transient" } };
|
|
192
|
+
}
|
|
193
|
+
return fakePublishIdentityOk();
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
]);
|
|
197
|
+
const auth = makeAuth(f);
|
|
198
|
+
await auth.signUp(validInput);
|
|
199
|
+
assert.equal(publishCalls, 2);
|
|
200
|
+
assert.ok(auth.canSignAsOwner());
|
|
201
|
+
});
|
|
202
|
+
it("throws ethos_bootstrap_failed after all retries fail with 5xx", async () => {
|
|
203
|
+
let publishCalls = 0;
|
|
204
|
+
const { fetch: f } = makeMockFetch([
|
|
205
|
+
{ url: "/auth/register", method: "POST", respond: fakeRegisterOk },
|
|
206
|
+
{
|
|
207
|
+
url: "/mcp/primitives/write",
|
|
208
|
+
method: "POST",
|
|
209
|
+
respond: () => {
|
|
210
|
+
publishCalls++;
|
|
211
|
+
return { status: 503, json: { error: "transient" } };
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
]);
|
|
215
|
+
const auth = makeAuth(f);
|
|
216
|
+
await assert.rejects(() => auth.signUp(validInput), (e) => e instanceof AithosSDKError && e.code === "ethos_bootstrap_failed");
|
|
217
|
+
// 3 attempts total (initial + 2 retries).
|
|
218
|
+
assert.equal(publishCalls, 3);
|
|
219
|
+
assert.equal(auth.canSignAsOwner(), false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
//# sourceMappingURL=signup-bootstrap.test.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "Aithos SDK — high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aithos",
|
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
"README.md",
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
45
|
+
"check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
46
|
+
"test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
|
|
47
|
+
"test:watch": "cd dist && node --test --watch",
|
|
48
|
+
"clean": "rm -rf dist",
|
|
49
|
+
"prepublishOnly": "npm run clean && npm run build && npm test"
|
|
50
|
+
},
|
|
42
51
|
"engines": {
|
|
43
52
|
"node": ">=20"
|
|
44
53
|
},
|
|
@@ -54,13 +63,5 @@
|
|
|
54
63
|
"publishConfig": {
|
|
55
64
|
"access": "public",
|
|
56
65
|
"tag": "alpha"
|
|
57
|
-
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"build": "tsc",
|
|
60
|
-
"build:test": "tsc -p tsconfig.test.json",
|
|
61
|
-
"check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
62
|
-
"test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
|
|
63
|
-
"test:watch": "cd dist && node --test --watch",
|
|
64
|
-
"clean": "rm -rf dist"
|
|
65
66
|
}
|
|
66
|
-
}
|
|
67
|
+
}
|