@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.
@@ -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-7`.
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
@@ -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
  *
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=signup-bootstrap.test.d.ts.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.19",
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
+ }