@aithos/sdk 0.1.0-alpha.23 → 0.1.0-alpha.25

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.
@@ -192,119 +192,6 @@ export interface InvokeSegmentationResult {
192
192
  readonly walletBalance: number;
193
193
  readonly auditId: string;
194
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
- }
308
195
  export interface ComputeNamespaceDeps {
309
196
  readonly auth: AithosAuth;
310
197
  readonly appDid: string;
@@ -392,41 +279,5 @@ export declare class ComputeNamespace {
392
279
  * Pricing: flat 5 000 mc per call (~$0.005 — Florence-2 is cheap).
393
280
  */
394
281
  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>;
431
282
  }
432
283
  //# sourceMappingURL=compute.d.ts.map
@@ -234,82 +234,6 @@ 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
- }
313
237
  /**
314
238
  * Resolve the active signer (owner takes precedence over delegate).
315
239
  *
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bundled copy of the `aithos.contacts.v1` schema.
3
+ *
4
+ * Mirrors `spec/data/schemas/aithos.contacts.v1.json` of the
5
+ * Aithos-protocol repo. The SDK uses this to split a record into its
6
+ * indexable (metadata) and encrypted (payload) parts on insert and to
7
+ * recombine them on read.
8
+ *
9
+ * In a later iteration the SDK will fetch / resolve schemas dynamically
10
+ * from a registry; for v0.1 we bundle the core schemas.
11
+ */
12
+ import type { AithosSchemaLite } from "./data.js";
13
+ export declare const contactsV1: AithosSchemaLite;
14
+ //# sourceMappingURL=data-schema-contacts-v1.d.ts.map
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ export const contactsV1 = {
4
+ schema: "aithos.contacts.v1",
5
+ indexable: new Set([
6
+ "name",
7
+ "email",
8
+ "phone_hash",
9
+ "status",
10
+ "tags",
11
+ "source",
12
+ "created_at",
13
+ "modified_at",
14
+ "last_contacted_at",
15
+ ]),
16
+ encrypted: new Set([
17
+ "phone",
18
+ "notes",
19
+ "conversation_log",
20
+ "form_responses",
21
+ "custom_fields",
22
+ ]),
23
+ auto: new Set(["created_at", "modified_at"]),
24
+ defaults: {
25
+ status: "lead",
26
+ },
27
+ };
28
+ //# sourceMappingURL=data-schema-contacts-v1.js.map
@@ -0,0 +1,97 @@
1
+ export interface AithosSchemaLite {
2
+ readonly schema: string;
3
+ readonly indexable: ReadonlySet<string>;
4
+ readonly encrypted: ReadonlySet<string>;
5
+ readonly auto: ReadonlySet<string>;
6
+ readonly defaults: Readonly<Record<string, unknown>>;
7
+ }
8
+ export interface CreateDataClientArgs {
9
+ /** Base URL of the deployed PDS (e.g. https://abc.execute-api.eu-west-3.amazonaws.com). */
10
+ readonly pdsUrl: string;
11
+ /** Subject DID — typically did:key:… in dev, did:aithos:… in prod. */
12
+ readonly did: string;
13
+ /**
14
+ * Ed25519 sphere seed (32 bytes). For did:key this is the same as
15
+ * the key embedded in the DID itself. For did:aithos this is the
16
+ * subject's `#data` (or `#public`) sphere key.
17
+ */
18
+ readonly sphereSeed: Uint8Array;
19
+ /**
20
+ * The verification method URL within the DID document used to sign
21
+ * envelopes. For did:key this is `<did>#<multibase>`. For did:aithos
22
+ * this is `<did>#data` (or `#public`).
23
+ */
24
+ readonly verificationMethod: string;
25
+ /** Optional fetch implementation. Defaults to globalThis.fetch. */
26
+ readonly fetch?: typeof fetch;
27
+ }
28
+ export interface DataClient {
29
+ /** Get / create a collection handle. */
30
+ collection(name: string): DataCollection;
31
+ /** Initialize a new collection with an explicit schema. */
32
+ createCollection(args: {
33
+ name: string;
34
+ schema: string;
35
+ forwardSecrecy?: "best_effort" | "strict";
36
+ }): Promise<void>;
37
+ /** List collections owned by this subject. */
38
+ listCollections(): Promise<readonly {
39
+ name: string;
40
+ schema: string;
41
+ record_count: number;
42
+ }[]>;
43
+ /** List gamma audit entries. */
44
+ listGammaEntries(opts?: {
45
+ limit?: number;
46
+ opPrefix?: string;
47
+ verify?: boolean;
48
+ }): Promise<unknown>;
49
+ /** Drop in-memory cache (CMK, collection metadata, …). */
50
+ reset(): void;
51
+ }
52
+ export interface DataCollection {
53
+ readonly name: string;
54
+ /**
55
+ * Insert a record. The object MAY contain both indexable and
56
+ * encrypted fields per the schema; the SDK splits them.
57
+ */
58
+ insert(record: Record<string, unknown>): Promise<string>;
59
+ /** Fetch one record by id (decrypted client-side). */
60
+ get(recordId: string): Promise<Record<string, unknown> | null>;
61
+ /** List records, decrypted. Pagination via opaque cursor. */
62
+ list(opts?: ListOpts): Promise<{
63
+ items: Record<string, unknown>[];
64
+ nextCursor?: string;
65
+ }>;
66
+ /**
67
+ * Replace a record. Same shape as insert; the SDK splits indexable
68
+ * vs encrypted again per the schema.
69
+ */
70
+ update(recordId: string, record: Record<string, unknown>): Promise<void>;
71
+ /** Soft-delete a record. */
72
+ delete(recordId: string): Promise<void>;
73
+ }
74
+ export interface ListOpts {
75
+ readonly filter?: {
76
+ readonly equals?: {
77
+ field: string;
78
+ value: unknown;
79
+ };
80
+ readonly contains?: {
81
+ field: string;
82
+ value: string;
83
+ };
84
+ readonly tagsAny?: readonly string[];
85
+ readonly tagsAll?: readonly string[];
86
+ readonly range?: {
87
+ field: string;
88
+ gte?: string;
89
+ lte?: string;
90
+ };
91
+ };
92
+ readonly order?: "newest" | "oldest";
93
+ readonly limit?: number;
94
+ readonly cursor?: string;
95
+ }
96
+ export declare function createDataClient(args: CreateDataClientArgs): DataClient;
97
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1,616 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright 2026 Mathieu Colla
3
+ /**
4
+ * `sdk.data` — high-level API for the Aithos data sub-protocol PDS.
5
+ *
6
+ * The Aithos data sub-protocol stores operational records (contacts,
7
+ * messages, ...) under a subject's identity, encrypted client-side,
8
+ * accessible to authorized apps via signed mandates. This module is the
9
+ * ergonomic façade developers consume:
10
+ *
11
+ * const client = createDataClient({ pdsUrl, did, sphereSeed });
12
+ * const contacts = client.collection("contacts");
13
+ * const id = await contacts.insert({ name: "Jean", phone: "+33..." });
14
+ * const list = await contacts.list({ filter: { status: "lead" } });
15
+ *
16
+ * Under the hood the module handles: envelope signing per spec §11,
17
+ * CMK / DEK lifecycle (generate, wrap, unwrap), per-record AEAD
18
+ * encryption, split between indexable metadata (server-visible) and
19
+ * encrypted payload (server-blind), JSON-RPC dispatch.
20
+ *
21
+ * It does not (yet) handle: mandate-delegate authentication on the
22
+ * caller side (the SDK only signs as owner in v0.1), schema
23
+ * publication (only the bundled `aithos.contacts.v1` is recognized),
24
+ * forward-secrecy CMK rotation primitives. Those land in later
25
+ * Sub-jalons.
26
+ *
27
+ * Spec ref: spec/data/01..10 of the aithos-protocol repo.
28
+ */
29
+ import { x25519 } from "@noble/curves/ed25519.js";
30
+ import { hkdf } from "@noble/hashes/hkdf.js";
31
+ import { sha256, sha512 } from "@noble/hashes/sha2.js";
32
+ import { XChaCha20Poly1305 } from "@stablelib/xchacha20poly1305";
33
+ import * as ed from "@noble/ed25519";
34
+ import { contactsV1 } from "./data-schema-contacts-v1.js";
35
+ // noble/ed25519 v2 needs sha512 wired in for sync sign/verify
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ ed.etc.sha512Sync = (...m) =>
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ sha512(ed.etc.concatBytes(...m));
40
+ /* -------------------------------------------------------------------------- */
41
+ /* Schema registry (local) */
42
+ /* -------------------------------------------------------------------------- */
43
+ const SCHEMAS = new Map([
44
+ [contactsV1.schema, contactsV1],
45
+ ]);
46
+ /* -------------------------------------------------------------------------- */
47
+ /* Public factory */
48
+ /* -------------------------------------------------------------------------- */
49
+ export function createDataClient(args) {
50
+ return new DataClientImpl(args);
51
+ }
52
+ /* -------------------------------------------------------------------------- */
53
+ /* Implementation */
54
+ /* -------------------------------------------------------------------------- */
55
+ class DataClientImpl {
56
+ #pdsUrl;
57
+ #did;
58
+ #seed;
59
+ #vm;
60
+ #fetch;
61
+ // Per-collection CMK cache: cleared on reset()
62
+ #cmkCache = new Map();
63
+ #colCache = new Map();
64
+ constructor(args) {
65
+ this.#pdsUrl = args.pdsUrl.replace(/\/$/, "");
66
+ this.#did = args.did;
67
+ this.#seed = args.sphereSeed;
68
+ this.#vm = args.verificationMethod;
69
+ this.#fetch = args.fetch ?? globalThis.fetch.bind(globalThis);
70
+ }
71
+ collection(name) {
72
+ return new DataCollectionImpl(this, name);
73
+ }
74
+ async createCollection(args) {
75
+ const cmk = randomBytes32();
76
+ const recipientDidUrl = `${this.#did}#data-kex`;
77
+ const collectionUrn = this.#collectionUrn(args.name);
78
+ const recipientPublic = ed25519SeedToX25519PublicKey(this.#seed);
79
+ const wrap = this.#wrapCmkForRecipient({
80
+ cmk,
81
+ recipientPublicKey: recipientPublic,
82
+ recipientDidUrl,
83
+ collectionUrn,
84
+ });
85
+ try {
86
+ await this.#call("/mcp/primitives/write", "aithos.data.create_collection", {
87
+ subject_did: this.#did,
88
+ collection_name: args.name,
89
+ schema: args.schema,
90
+ ...(args.forwardSecrecy ? { forward_secrecy: args.forwardSecrecy } : {}),
91
+ cmk_envelope: {
92
+ alg: "xchacha20poly1305-ietf",
93
+ wraps: [wrap],
94
+ },
95
+ });
96
+ // Cache CMK in memory for subsequent ops on this collection.
97
+ this.#cmkCache.set(args.name, cmk);
98
+ }
99
+ finally {
100
+ // CMK is retained in cache, only zero the local var if we didn't cache.
101
+ }
102
+ }
103
+ async listCollections() {
104
+ const r = await this.#call("/mcp/primitives/read", "aithos.data.list_collections", {
105
+ subject_did: this.#did,
106
+ });
107
+ const items = r.items ?? [];
108
+ return items.map((c) => ({
109
+ name: c.name,
110
+ schema: c.schema,
111
+ record_count: c.record_count,
112
+ }));
113
+ }
114
+ async listGammaEntries(opts = {}) {
115
+ const params = { subject_did: this.#did };
116
+ if (opts.limit !== undefined)
117
+ params.limit = opts.limit;
118
+ if (opts.opPrefix)
119
+ params.op_prefix = opts.opPrefix;
120
+ if (opts.verify)
121
+ params.verify = true;
122
+ return this.#call("/mcp/primitives/read", "aithos.data.list_gamma_entries", params);
123
+ }
124
+ reset() {
125
+ for (const k of this.#cmkCache.values())
126
+ k.fill(0);
127
+ this.#cmkCache.clear();
128
+ this.#colCache.clear();
129
+ }
130
+ /* -- Internals used by DataCollection -- */
131
+ async _ensureCollection(name) {
132
+ const cached = this.#colCache.get(name);
133
+ if (cached)
134
+ return cached;
135
+ // Fetch metadata from PDS
136
+ const meta = (await this.#call("/mcp/primitives/read", "aithos.data.get_collection", {
137
+ subject_did: this.#did,
138
+ collection_name: name,
139
+ }));
140
+ // Look up our wrap and unwrap the CMK
141
+ const ourRecipient = `${this.#did}#data-kex`;
142
+ const wrap = meta.cmk_envelope.wraps.find((w) => w.recipient === ourRecipient);
143
+ if (!wrap) {
144
+ throw new Error(`sdk.data: no CMK wrap for ${ourRecipient} in collection ${name}. The collection was either created with a different recipient, or this client is not the owner.`);
145
+ }
146
+ const cmk = this.#unwrapCmk({
147
+ wrap,
148
+ collectionUrn: meta.urn,
149
+ privateKey: ed25519SeedToX25519PrivateKey(this.#seed),
150
+ });
151
+ this.#cmkCache.set(name, cmk);
152
+ const schema = SCHEMAS.get(meta.schema);
153
+ if (!schema) {
154
+ throw new Error(`sdk.data: schema "${meta.schema}" not known to the SDK`);
155
+ }
156
+ const state = { name, urn: meta.urn, schema };
157
+ this.#colCache.set(name, state);
158
+ return state;
159
+ }
160
+ async _insert(state, record) {
161
+ const cmk = this.#cmkCache.get(state.name);
162
+ if (!cmk)
163
+ throw new Error("CMK not loaded");
164
+ const { metadata, payload } = splitRecord(record, state.schema);
165
+ const recordId = `record_${makeUlid()}`;
166
+ const encrypted = this.#encryptPayload({
167
+ collectionName: state.name,
168
+ recordId,
169
+ payload,
170
+ cmk,
171
+ });
172
+ const r = (await this.#call("/mcp/primitives/write", "aithos.data.insert_record", {
173
+ collection_urn: state.urn,
174
+ record_id: recordId,
175
+ metadata,
176
+ payload: encrypted,
177
+ }));
178
+ return r.record_id;
179
+ }
180
+ async _get(state, recordId) {
181
+ let raw;
182
+ try {
183
+ raw = await this.#call("/mcp/primitives/read", "aithos.data.get_record", {
184
+ collection_urn: state.urn,
185
+ record_id: recordId,
186
+ });
187
+ }
188
+ catch (e) {
189
+ if (e.code === -32020)
190
+ return null;
191
+ throw e;
192
+ }
193
+ return this.#decryptRecord(state, raw);
194
+ }
195
+ async _list(state, opts = {}) {
196
+ const params = {
197
+ collection_urn: state.urn,
198
+ };
199
+ if (opts.filter)
200
+ params.filter = opts.filter;
201
+ if (opts.order)
202
+ params.order = opts.order;
203
+ if (opts.limit !== undefined)
204
+ params.limit = opts.limit;
205
+ if (opts.cursor)
206
+ params.cursor = opts.cursor;
207
+ const r = (await this.#call("/mcp/primitives/read", "aithos.data.list_records", params));
208
+ const items = r.items.map((it) => this.#decryptRecord(state, it));
209
+ return {
210
+ items,
211
+ ...(r.next_cursor ? { nextCursor: r.next_cursor } : {}),
212
+ };
213
+ }
214
+ async _update(state, recordId, record) {
215
+ const cmk = this.#cmkCache.get(state.name);
216
+ if (!cmk)
217
+ throw new Error("CMK not loaded");
218
+ const { metadata, payload } = splitRecord(record, state.schema);
219
+ const encrypted = this.#encryptPayload({
220
+ collectionName: state.name,
221
+ recordId,
222
+ payload,
223
+ cmk,
224
+ });
225
+ await this.#call("/mcp/primitives/write", "aithos.data.update_record", {
226
+ collection_urn: state.urn,
227
+ record_id: recordId,
228
+ metadata,
229
+ payload: encrypted,
230
+ });
231
+ }
232
+ async _delete(state, recordId) {
233
+ await this.#call("/mcp/primitives/write", "aithos.data.delete_record", {
234
+ collection_urn: state.urn,
235
+ record_id: recordId,
236
+ });
237
+ }
238
+ /* -- JSON-RPC dispatch -- */
239
+ async #call(path, method, params) {
240
+ const aud = `${this.#pdsUrl}${path}`;
241
+ const envelope = this.#signEnvelope({ aud, method, params });
242
+ const body = {
243
+ jsonrpc: "2.0",
244
+ id: makeUlid(),
245
+ method,
246
+ params: { ...params, _envelope: envelope },
247
+ };
248
+ const r = await this.#fetch(aud, {
249
+ method: "POST",
250
+ headers: { "content-type": "application/json" },
251
+ body: JSON.stringify(body),
252
+ });
253
+ const json = (await r.json());
254
+ if (json.error) {
255
+ const err = new Error(json.error.message);
256
+ err.code = json.error.code;
257
+ err.data = json.error.data;
258
+ throw err;
259
+ }
260
+ return json.result ?? {};
261
+ }
262
+ /* -- Envelope signing (inlined subset of @aithos/protocol-core/envelope) -- */
263
+ #signEnvelope(args) {
264
+ const now = Math.floor(Date.now() / 1000);
265
+ const exp = now + 60;
266
+ const nonce = makeUlid();
267
+ const paramsHash = "sha256-" + sha256Hex(jcsCanonicalize(args.params));
268
+ const unsigned = {
269
+ "aithos-envelope": "0.1.0",
270
+ iss: this.#did,
271
+ aud: args.aud,
272
+ method: args.method,
273
+ iat: now,
274
+ exp,
275
+ nonce,
276
+ params_hash: paramsHash,
277
+ proof: {
278
+ type: "Ed25519Signature2020",
279
+ verificationMethod: this.#vm,
280
+ created: new Date(now * 1000).toISOString(),
281
+ proofValue: "",
282
+ },
283
+ };
284
+ const bytes = new TextEncoder().encode(jcsCanonicalize(unsigned));
285
+ const sig = ed.sign(bytes, this.#seed);
286
+ return {
287
+ ...unsigned,
288
+ proof: { ...unsigned.proof, proofValue: base64url(sig) },
289
+ };
290
+ }
291
+ /* -- Crypto helpers -- */
292
+ #collectionUrn(name) {
293
+ return `urn:aithos:collection:${this.#did}:${name}`;
294
+ }
295
+ #wrapCmkForRecipient(args) {
296
+ const ephSk = x25519.utils.randomSecretKey();
297
+ const ephPk = x25519.getPublicKey(ephSk);
298
+ const shared = x25519.getSharedSecret(ephSk, args.recipientPublicKey);
299
+ const wrapKey = hkdf(sha256, shared, utf8("aithos-data-cmk-wrap-v1"), utf8(args.recipientDidUrl), 32);
300
+ const wrapNonce = randomBytes24();
301
+ const aad = aadCmkWrap(args.collectionUrn, args.recipientDidUrl);
302
+ const aead = new XChaCha20Poly1305(wrapKey);
303
+ const wrapped = aead.seal(wrapNonce, args.cmk, aad);
304
+ wrapKey.fill(0);
305
+ shared.fill(0);
306
+ return {
307
+ recipient: args.recipientDidUrl,
308
+ alg: "x25519-hkdf-sha256-aead",
309
+ ephemeral_public: base64Std(ephPk),
310
+ wrap_nonce: base64Std(wrapNonce),
311
+ wrapped_key: base64Std(wrapped),
312
+ };
313
+ }
314
+ #unwrapCmk(args) {
315
+ const ephPk = fromBase64(args.wrap.ephemeral_public);
316
+ const wrapNonce = fromBase64(args.wrap.wrap_nonce);
317
+ const wrappedKey = fromBase64(args.wrap.wrapped_key);
318
+ const shared = x25519.getSharedSecret(args.privateKey, ephPk);
319
+ const wrapKey = hkdf(sha256, shared, utf8("aithos-data-cmk-wrap-v1"), utf8(args.wrap.recipient), 32);
320
+ const aad = aadCmkWrap(args.collectionUrn, args.wrap.recipient);
321
+ const aead = new XChaCha20Poly1305(wrapKey);
322
+ const cmk = aead.open(wrapNonce, wrappedKey, aad);
323
+ wrapKey.fill(0);
324
+ shared.fill(0);
325
+ if (!cmk)
326
+ throw new Error("sdk.data: CMK unwrap failed (wrong key or AAD mismatch)");
327
+ return cmk;
328
+ }
329
+ #encryptPayload(args) {
330
+ const dek = randomBytes32();
331
+ try {
332
+ const plaintext = new TextEncoder().encode(jcsCanonicalize(args.payload));
333
+ const nonce = randomBytes24();
334
+ const aad = aadRecord(this.#did, args.collectionName, args.recordId);
335
+ const aead = new XChaCha20Poly1305(dek);
336
+ const ciphertext = aead.seal(nonce, plaintext, aad);
337
+ const dekWrapNonce = randomBytes24();
338
+ const dekAad = aadDekWrap(this.#did, args.collectionName, args.recordId);
339
+ const dekAead = new XChaCha20Poly1305(args.cmk);
340
+ const wrapped = dekAead.seal(dekWrapNonce, dek, dekAad);
341
+ const dekWrap = new Uint8Array(dekWrapNonce.length + wrapped.length);
342
+ dekWrap.set(dekWrapNonce, 0);
343
+ dekWrap.set(wrapped, dekWrapNonce.length);
344
+ return {
345
+ alg: "xchacha20poly1305-ietf",
346
+ nonce: base64Std(nonce),
347
+ ciphertext: base64Std(ciphertext),
348
+ dek_wrapped_for_cmk: base64Std(dekWrap),
349
+ };
350
+ }
351
+ finally {
352
+ dek.fill(0);
353
+ }
354
+ }
355
+ #decryptRecord(state, raw) {
356
+ if (raw.deleted) {
357
+ // Soft-deleted record — payload was cleared.
358
+ return { ...raw.metadata, _deleted: true };
359
+ }
360
+ const cmk = this.#cmkCache.get(state.name);
361
+ if (!cmk) {
362
+ throw new Error("sdk.data: CMK not loaded — call ensureCollection first");
363
+ }
364
+ // Unwrap DEK
365
+ const wrapBuf = fromBase64(raw.payload.dek_wrapped_for_cmk);
366
+ const wrapNonce = wrapBuf.slice(0, 24);
367
+ const wrapped = wrapBuf.slice(24);
368
+ const dekAad = aadDekWrap(this.#did, state.name, raw.record_id);
369
+ const dekAead = new XChaCha20Poly1305(cmk);
370
+ const dek = dekAead.open(wrapNonce, wrapped, dekAad);
371
+ if (!dek)
372
+ throw new Error("sdk.data: DEK unwrap failed");
373
+ try {
374
+ const nonce = fromBase64(raw.payload.nonce);
375
+ const ciphertext = fromBase64(raw.payload.ciphertext);
376
+ const aad = aadRecord(this.#did, state.name, raw.record_id);
377
+ const aead = new XChaCha20Poly1305(dek);
378
+ const plaintext = aead.open(nonce, ciphertext, aad);
379
+ if (!plaintext)
380
+ throw new Error("sdk.data: payload decrypt failed");
381
+ const payload = JSON.parse(new TextDecoder().decode(plaintext));
382
+ return { record_id: raw.record_id, ...raw.metadata, ...payload };
383
+ }
384
+ finally {
385
+ dek.fill(0);
386
+ }
387
+ }
388
+ }
389
+ class DataCollectionImpl {
390
+ client;
391
+ name;
392
+ constructor(client, name) {
393
+ this.client = client;
394
+ this.name = name;
395
+ }
396
+ async insert(record) {
397
+ const state = await this.client._ensureCollection(this.name);
398
+ return this.client._insert(state, record);
399
+ }
400
+ async get(recordId) {
401
+ const state = await this.client._ensureCollection(this.name);
402
+ return this.client._get(state, recordId);
403
+ }
404
+ async list(opts) {
405
+ const state = await this.client._ensureCollection(this.name);
406
+ return this.client._list(state, opts);
407
+ }
408
+ async update(recordId, record) {
409
+ const state = await this.client._ensureCollection(this.name);
410
+ return this.client._update(state, recordId, record);
411
+ }
412
+ async delete(recordId) {
413
+ const state = await this.client._ensureCollection(this.name);
414
+ return this.client._delete(state, recordId);
415
+ }
416
+ }
417
+ /* -------------------------------------------------------------------------- */
418
+ /* Record split (metadata vs payload) */
419
+ /* -------------------------------------------------------------------------- */
420
+ function splitRecord(record, schema) {
421
+ const metadata = {};
422
+ const payload = {};
423
+ for (const [k, v] of Object.entries(record)) {
424
+ if (schema.auto.has(k))
425
+ continue; // server-set
426
+ if (schema.indexable.has(k)) {
427
+ metadata[k] = v;
428
+ }
429
+ else if (schema.encrypted.has(k)) {
430
+ payload[k] = v;
431
+ }
432
+ else {
433
+ // Unknown field — drop. Server will reject in any case (additionalProperties: false).
434
+ }
435
+ }
436
+ return { metadata, payload };
437
+ }
438
+ /* -------------------------------------------------------------------------- */
439
+ /* Crypto helpers */
440
+ /* -------------------------------------------------------------------------- */
441
+ function randomBytes32() {
442
+ return cryptoRandom(32);
443
+ }
444
+ function randomBytes24() {
445
+ return cryptoRandom(24);
446
+ }
447
+ /** Cross-platform CSPRNG: Web Crypto in browser, Node WebCrypto in Node 19+. */
448
+ function cryptoRandom(n) {
449
+ const buf = new Uint8Array(n);
450
+ // globalThis.crypto exists in browsers and in Node 19+
451
+ globalThis.crypto?.getRandomValues(buf);
452
+ return buf;
453
+ }
454
+ function utf8(s) {
455
+ return new TextEncoder().encode(s);
456
+ }
457
+ function aadCmkWrap(collectionUrn, recipient) {
458
+ const p = utf8("aithos-data-cmk-v1\0");
459
+ const c = utf8(collectionUrn);
460
+ const sep = new Uint8Array([0]);
461
+ const r = utf8(recipient);
462
+ const out = new Uint8Array(p.length + c.length + sep.length + r.length);
463
+ let off = 0;
464
+ out.set(p, off);
465
+ off += p.length;
466
+ out.set(c, off);
467
+ off += c.length;
468
+ out.set(sep, off);
469
+ off += sep.length;
470
+ out.set(r, off);
471
+ return out;
472
+ }
473
+ function aadDekWrap(subjectDid, collectionName, recordId) {
474
+ const p = utf8("aithos-data-dek-v1\0");
475
+ return concat3WithSeps(p, subjectDid, collectionName, recordId);
476
+ }
477
+ function aadRecord(subjectDid, collectionName, recordId) {
478
+ const p = utf8("aithos-data-record-v1\0");
479
+ return concat3WithSeps(p, subjectDid, collectionName, recordId);
480
+ }
481
+ function concat3WithSeps(prefix, a, b, c) {
482
+ const aa = utf8(a);
483
+ const bb = utf8(b);
484
+ const cc = utf8(c);
485
+ const sep = new Uint8Array([0]);
486
+ const total = prefix.length + aa.length + sep.length + bb.length + sep.length + cc.length;
487
+ const out = new Uint8Array(total);
488
+ let off = 0;
489
+ out.set(prefix, off);
490
+ off += prefix.length;
491
+ out.set(aa, off);
492
+ off += aa.length;
493
+ out.set(sep, off);
494
+ off += sep.length;
495
+ out.set(bb, off);
496
+ off += bb.length;
497
+ out.set(sep, off);
498
+ off += sep.length;
499
+ out.set(cc, off);
500
+ return out;
501
+ }
502
+ /**
503
+ * Derive a 32-byte X25519 private key from an Ed25519 seed via SHA-512
504
+ * truncation + clamping. Mirrors libsodium's
505
+ * crypto_sign_ed25519_sk_to_curve25519 for the secret-key side. Used so
506
+ * a single Ed25519 sphere seed can both sign envelopes and key-agree
507
+ * with mandate recipients.
508
+ */
509
+ function ed25519SeedToX25519PrivateKey(seed) {
510
+ if (seed.length !== 32)
511
+ throw new Error("Ed25519 seed must be 32 bytes");
512
+ const h = sha512(seed);
513
+ const sk = new Uint8Array(h.slice(0, 32));
514
+ // Clamp per X25519 spec
515
+ sk[0] = sk[0] & 248;
516
+ sk[31] = sk[31] & 127;
517
+ sk[31] = sk[31] | 64;
518
+ return sk;
519
+ }
520
+ function ed25519SeedToX25519PublicKey(seed) {
521
+ const sk = ed25519SeedToX25519PrivateKey(seed);
522
+ try {
523
+ return x25519.getPublicKey(sk);
524
+ }
525
+ finally {
526
+ sk.fill(0);
527
+ }
528
+ }
529
+ function makeUlid() {
530
+ // Lightweight ULID — millisecond timestamp + 80 bits of randomness.
531
+ // Crockford base32. For tests this is sufficient; production uses
532
+ // the canonical ulid package.
533
+ const tsBuf = new Uint8Array(6);
534
+ let ts = Date.now();
535
+ for (let i = 5; i >= 0; i--) {
536
+ tsBuf[i] = ts & 0xff;
537
+ ts = Math.floor(ts / 256);
538
+ }
539
+ const rndBuf = cryptoRandom(10);
540
+ const all = new Uint8Array(16);
541
+ all.set(tsBuf, 0);
542
+ all.set(rndBuf, 6);
543
+ const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
544
+ let bits = 0;
545
+ let value = 0;
546
+ let out = "";
547
+ for (const b of all) {
548
+ value = (value << 8) | b;
549
+ bits += 8;
550
+ while (bits >= 5) {
551
+ out += alphabet[(value >> (bits - 5)) & 0x1f];
552
+ bits -= 5;
553
+ }
554
+ }
555
+ if (bits > 0)
556
+ out += alphabet[(value << (5 - bits)) & 0x1f];
557
+ return out.slice(0, 26);
558
+ }
559
+ /** Standard base64 (with `=` padding). Browser + Node compatible. */
560
+ function base64Std(bytes) {
561
+ let bin = "";
562
+ for (let i = 0; i < bytes.length; i++)
563
+ bin += String.fromCharCode(bytes[i]);
564
+ return btoa(bin);
565
+ }
566
+ /** base64url (URL-safe, no padding). */
567
+ function base64url(bytes) {
568
+ return base64Std(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
569
+ }
570
+ function fromBase64(s) {
571
+ // Accept either standard base64 or base64url
572
+ const std = s.replace(/-/g, "+").replace(/_/g, "/");
573
+ const padded = std + "=".repeat((4 - (std.length % 4)) % 4);
574
+ const bin = atob(padded);
575
+ const out = new Uint8Array(bin.length);
576
+ for (let i = 0; i < bin.length; i++)
577
+ out[i] = bin.charCodeAt(i);
578
+ return out;
579
+ }
580
+ function sha256Hex(s) {
581
+ const d = sha256(new TextEncoder().encode(s));
582
+ let hex = "";
583
+ for (const b of d)
584
+ hex += b.toString(16).padStart(2, "0");
585
+ return hex;
586
+ }
587
+ /* -------------------------------------------------------------------------- */
588
+ /* JCS-style canonicalization (RFC 8785 subset) */
589
+ /* -------------------------------------------------------------------------- */
590
+ function jcsCanonicalize(value) {
591
+ if (value === null)
592
+ return "null";
593
+ if (value === undefined)
594
+ throw new Error("Cannot canonicalize undefined");
595
+ if (typeof value === "boolean")
596
+ return value ? "true" : "false";
597
+ if (typeof value === "number") {
598
+ if (!Number.isFinite(value))
599
+ throw new Error("non-finite number");
600
+ return value.toString();
601
+ }
602
+ if (typeof value === "string")
603
+ return JSON.stringify(value);
604
+ if (Array.isArray(value)) {
605
+ return "[" + value.map(jcsCanonicalize).join(",") + "]";
606
+ }
607
+ if (typeof value === "object") {
608
+ const obj = value;
609
+ const keys = Object.keys(obj).sort();
610
+ return ("{" +
611
+ keys.map((k) => JSON.stringify(k) + ":" + jcsCanonicalize(obj[k])).join(",") +
612
+ "}");
613
+ }
614
+ throw new Error(`Cannot canonicalize ${typeof value}`);
615
+ }
616
+ //# sourceMappingURL=data.js.map
@@ -1,11 +1,11 @@
1
- export declare const VERSION = "0.1.0-alpha.23";
1
+ export declare const VERSION = "0.1.0-alpha.25";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
5
5
  export { AithosRpcError } from "@aithos/protocol-client";
6
6
  export type { AithosSdkEndpoints } from "./endpoints.js";
7
7
  export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
8
- export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, InvokeUrlFetchArgs, InvokeUrlFetchResult, SegmentPolygon, StopReason, UrlFetchCitation, UrlFetchMetadata, UrlFetchModelId, } from "./compute.js";
8
+ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs, InvokeBedrockResult, InvokeBedrockVisionArgs, InvokeBedrockVisionResult, InvokeImageArgs, InvokeImageImage, InvokeImageResult, InvokeSegmentationArgs, InvokeSegmentationResult, SegmentPolygon, StopReason, } 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";
@@ -21,4 +21,5 @@ export { COMPUTE_INVOKE_SCOPE, MandatesNamespace } from "./mandates.js";
21
21
  export type { ActorSphere, CreateMandateComputeInput, CreateMandateInput, MintedMandate, OwnedMandate, Scope, } from "./mandates.js";
22
22
  export * as onboarding from "./onboarding.js";
23
23
  export { createBrowserIdentity, browserIdentityFromStored, type BrowserIdentity, } from "@aithos/protocol-client";
24
+ export { createDataClient, type CreateDataClientArgs, type DataClient, type DataCollection, type ListOpts, type AithosSchemaLite, } from "./data.js";
24
25
  //# sourceMappingURL=index.d.ts.map
package/dist/src/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
18
18
  // are exported from here. Endpoint config (`AithosSdkEndpoints`,
19
19
  // `DEFAULT_SDK_ENDPOINTS`) likewise.
20
- export const VERSION = "0.1.0-alpha.23";
20
+ export const VERSION = "0.1.0-alpha.25";
21
21
  export { AithosSDK } from "./sdk.js";
22
22
  export { AithosSDKError } from "./types.js";
23
23
  // Re-export protocol-client's JSON-RPC error type so consumers can
@@ -58,4 +58,8 @@ export * as onboarding from "./onboarding.js";
58
58
  // Convenience direct re-exports of the most-used identity primitives so the
59
59
  // quick-start example doesn't need a namespace import.
60
60
  export { createBrowserIdentity, browserIdentityFromStored, } from "@aithos/protocol-client";
61
+ // `sdk.data` namespace — Aithos data sub-protocol PDS client. Manages
62
+ // the lifecycle of subject-owned, encrypted, schema-validated records.
63
+ // See spec/data/ in the aithos-protocol repo.
64
+ export { createDataClient, } from "./data.js";
61
65
  //# sourceMappingURL=index.js.map
@@ -187,166 +187,8 @@ 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
+ // `compute.invokeUrlFetch` was removed in alpha.24 (BREAKING). The
191
+ // Anthropic API-direct + web_fetch tool path is replaced by the
192
+ // `sdk.web.extract` namespace which routes through the deterministic
193
+ // web-extractor Lambda. No tests here anymore.
352
194
  //# sourceMappingURL=compute.test.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.23",
3
+ "version": "0.1.0-alpha.25",
4
4
  "description": "Aithos SDK \u2014 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",