@aithos/sdk 0.1.0-alpha.20 → 0.1.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/src/endpoints.d.ts +9 -0
- package/dist/src/endpoints.js +5 -0
- package/dist/src/ethos.d.ts +47 -0
- package/dist/src/ethos.js +83 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/sdk.d.ts +3 -0
- package/dist/src/sdk.js +9 -0
- package/dist/src/web.d.ts +279 -0
- package/dist/src/web.js +186 -0
- package/dist/test/endpoints.test.js +20 -1
- package/dist/test/web.test.d.ts +2 -0
- package/dist/test/web.test.js +270 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -100,11 +100,47 @@ network — they fail fast with a precise `AithosSDKError`:
|
|
|
100
100
|
set — useful for agents that only consume tokens (e.g. creative
|
|
101
101
|
assistants) without seeing any of your data.
|
|
102
102
|
|
|
103
|
+
## Extracting webpages without an LLM
|
|
104
|
+
|
|
105
|
+
`sdk.web` is a token-priced primitive that lets your agent read a
|
|
106
|
+
public webpage and get back cleaned HTML, purged CSS and a
|
|
107
|
+
deterministic visual signature — all computed server-side without an
|
|
108
|
+
LLM in the loop. Pricing is a flat **1 microcredit** per successful
|
|
109
|
+
extraction (refunded on failure), versus ~30 mc for a comparable
|
|
110
|
+
LLM-based extraction.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { AithosSDK } from "@aithos/sdk";
|
|
114
|
+
|
|
115
|
+
const sdk = new AithosSDK({ auth, appDid });
|
|
116
|
+
|
|
117
|
+
const { data, creditsCharged } = await sdk.web.extract({
|
|
118
|
+
url: "https://example.com",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log(data.meta.title); // "Example Domain"
|
|
122
|
+
console.log(data.visual_signature.colors.primary); // "#0078d4"
|
|
123
|
+
console.log(data.styles.css.length); // purged + minified CSS
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Owners can mint a mandate for delegate-only extraction:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { WEB_EXTRACT_SCOPE } from "@aithos/sdk";
|
|
130
|
+
|
|
131
|
+
await sdk.mandates.create({
|
|
132
|
+
appDid: "did:aithos:app:my-agent",
|
|
133
|
+
scopes: [WEB_EXTRACT_SCOPE],
|
|
134
|
+
// ...
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
103
138
|
## What lives where
|
|
104
139
|
|
|
105
140
|
| Namespace | Purpose |
|
|
106
141
|
| ---------------- | ------------------------------------------------------------------------------------------ |
|
|
107
142
|
| `sdk.compute` | Bedrock invocation through the Aithos compute proxy (signed envelope, wallet enforcement). |
|
|
143
|
+
| `sdk.web` | Webpage extraction without an LLM through the web extractor proxy (1 mc / call). |
|
|
108
144
|
| `sdk.wallet` | Stripe Checkout sessions for credit-pack top-ups, balance helpers. |
|
|
109
145
|
| `sdk.ethos` | Ethos-zone composition / parsing — re-exported from `@aithos/protocol-client`. |
|
|
110
146
|
| `sdk.onboarding` | First-run identity / DID flows — re-exported. |
|
package/dist/src/endpoints.d.ts
CHANGED
|
@@ -10,11 +10,20 @@ export interface AithosSdkEndpoints {
|
|
|
10
10
|
* suffixes a fixed path to it.
|
|
11
11
|
*/
|
|
12
12
|
readonly wallet: string;
|
|
13
|
+
/**
|
|
14
|
+
* Web extractor proxy base URL — `aithos.web_extract`. Default
|
|
15
|
+
* `https://extract.aithos.be`. Same JSON-RPC + signed-envelope shape
|
|
16
|
+
* as the compute proxy, distinct audience so a mandate can hold one
|
|
17
|
+
* scope without the other.
|
|
18
|
+
*/
|
|
19
|
+
readonly web: string;
|
|
13
20
|
}
|
|
14
21
|
/** Production defaults. */
|
|
15
22
|
export declare const DEFAULT_SDK_ENDPOINTS: AithosSdkEndpoints;
|
|
16
23
|
/** Compose the full compute-invoke URL: `${compute}/v1/invoke`. */
|
|
17
24
|
export declare function computeInvokeUrl(endpoints: AithosSdkEndpoints): string;
|
|
25
|
+
/** Compose the full web-extract URL: `${web}/v1/invoke`. */
|
|
26
|
+
export declare function webInvokeUrl(endpoints: AithosSdkEndpoints): string;
|
|
18
27
|
/** Compose the full top-up-checkout URL: `${wallet}/v1/wallet/topup/checkout`. */
|
|
19
28
|
export declare function walletTopupCheckoutUrl(endpoints: AithosSdkEndpoints): string;
|
|
20
29
|
/**
|
package/dist/src/endpoints.js
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
export const DEFAULT_SDK_ENDPOINTS = {
|
|
5
5
|
compute: "https://compute.aithos.be",
|
|
6
6
|
wallet: "https://wallet.aithos.be",
|
|
7
|
+
web: "https://extract.aithos.be",
|
|
7
8
|
};
|
|
8
9
|
/** Compose the full compute-invoke URL: `${compute}/v1/invoke`. */
|
|
9
10
|
export function computeInvokeUrl(endpoints) {
|
|
10
11
|
return `${trimSlash(endpoints.compute)}/v1/invoke`;
|
|
11
12
|
}
|
|
13
|
+
/** Compose the full web-extract URL: `${web}/v1/invoke`. */
|
|
14
|
+
export function webInvokeUrl(endpoints) {
|
|
15
|
+
return `${trimSlash(endpoints.web)}/v1/invoke`;
|
|
16
|
+
}
|
|
12
17
|
/** Compose the full top-up-checkout URL: `${wallet}/v1/wallet/topup/checkout`. */
|
|
13
18
|
export function walletTopupCheckoutUrl(endpoints) {
|
|
14
19
|
return `${trimSlash(endpoints.wallet)}/v1/wallet/topup/checkout`;
|
package/dist/src/ethos.d.ts
CHANGED
|
@@ -85,6 +85,53 @@ export declare class EthosZone {
|
|
|
85
85
|
addSection(input: AddSectionInput): void;
|
|
86
86
|
updateSection(sectionId: string, patch: UpdateSectionPatch): void;
|
|
87
87
|
deleteSection(sectionId: string): void;
|
|
88
|
+
/**
|
|
89
|
+
* Return every section in this zone whose `title` is exactly `title`.
|
|
90
|
+
*
|
|
91
|
+
* Match is exact and case-sensitive. The result is always an array — it
|
|
92
|
+
* may be empty (no match), have one element (the typical case), or have
|
|
93
|
+
* more than one element when the author has happened to publish two
|
|
94
|
+
* sections with the same title. Section titles are not required by the
|
|
95
|
+
* protocol to be unique within a zone.
|
|
96
|
+
*
|
|
97
|
+
* The order of returned sections is the zone's authored order
|
|
98
|
+
* (`sections()` ordering, spec §2.5.2).
|
|
99
|
+
*
|
|
100
|
+
* @param title Section title to look up — exact, case-sensitive.
|
|
101
|
+
*/
|
|
102
|
+
findSectionsByTitle(title: string): Promise<readonly Section[]>;
|
|
103
|
+
/**
|
|
104
|
+
* Stage an update for **every** section in this zone whose `title`
|
|
105
|
+
* matches `title` exactly. Returns the list of section IDs that were
|
|
106
|
+
* staged — empty when nothing matched.
|
|
107
|
+
*
|
|
108
|
+
* Apply with `client.publish()` like any other staged mutation. The
|
|
109
|
+
* staged entries are identical to what `updateSection(id, patch)` would
|
|
110
|
+
* produce, one per matched section, so `pendingChanges()` /
|
|
111
|
+
* `discard()` behave normally.
|
|
112
|
+
*
|
|
113
|
+
* Note: this method does NOT throw when there is no match — it returns
|
|
114
|
+
* `[]`. That's intentional: callers driven by an LLM frequently want to
|
|
115
|
+
* upsert (try update, then fall back to add) and shouldn't have to
|
|
116
|
+
* catch.
|
|
117
|
+
*
|
|
118
|
+
* @param title Section title to look up — exact, case-sensitive.
|
|
119
|
+
* @param patch Same patch shape accepted by `updateSection`.
|
|
120
|
+
* @returns Array of `section.id` strings whose updates were staged.
|
|
121
|
+
*/
|
|
122
|
+
updateSectionsByTitle(title: string, patch: UpdateSectionPatch): Promise<readonly string[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Stage a delete for **every** section in this zone whose `title`
|
|
125
|
+
* matches `title` exactly. Returns the list of section IDs that were
|
|
126
|
+
* staged — empty when nothing matched.
|
|
127
|
+
*
|
|
128
|
+
* Same semantics as {@link updateSectionsByTitle}: silent on no-match,
|
|
129
|
+
* apply with `client.publish()`.
|
|
130
|
+
*
|
|
131
|
+
* @param title Section title to look up — exact, case-sensitive.
|
|
132
|
+
* @returns Array of `section.id` strings whose deletes were staged.
|
|
133
|
+
*/
|
|
134
|
+
deleteSectionsByTitle(title: string): Promise<readonly string[]>;
|
|
88
135
|
}
|
|
89
136
|
export interface EthosNamespaceDeps {
|
|
90
137
|
readonly auth: AithosAuth;
|
package/dist/src/ethos.js
CHANGED
|
@@ -517,6 +517,89 @@ export class EthosZone {
|
|
|
517
517
|
deleteSection(sectionId) {
|
|
518
518
|
this.#parent._stageDelete(this.#name, sectionId);
|
|
519
519
|
}
|
|
520
|
+
/* ------------------------------------------------------------------------ */
|
|
521
|
+
/* By-title helpers */
|
|
522
|
+
/* */
|
|
523
|
+
/* The Aithos protocol does not (yet) treat section titles as a queryable */
|
|
524
|
+
/* index — only `section.id` is a normative anchor. These three methods */
|
|
525
|
+
/* are an ergonomic SDK-side affordance for the common LLM-driven case */
|
|
526
|
+
/* "act on the section called X". They resolve titles client-side by */
|
|
527
|
+
/* loading the full zone (`sections()`) and exact-match filtering on */
|
|
528
|
+
/* `Section.title`. */
|
|
529
|
+
/* */
|
|
530
|
+
/* When a future revision of `aithos.get_ethos_zone` (or a new */
|
|
531
|
+
/* `aithos.find_sections` primitive) supports server-side title */
|
|
532
|
+
/* filtering, the implementation behind these three methods can swap to */
|
|
533
|
+
/* the server call without changing the public signatures here. The */
|
|
534
|
+
/* contract — async, exact case-sensitive match, plural semantics — is */
|
|
535
|
+
/* intentionally aligned with what such an API would return. */
|
|
536
|
+
/* ------------------------------------------------------------------------ */
|
|
537
|
+
/**
|
|
538
|
+
* Return every section in this zone whose `title` is exactly `title`.
|
|
539
|
+
*
|
|
540
|
+
* Match is exact and case-sensitive. The result is always an array — it
|
|
541
|
+
* may be empty (no match), have one element (the typical case), or have
|
|
542
|
+
* more than one element when the author has happened to publish two
|
|
543
|
+
* sections with the same title. Section titles are not required by the
|
|
544
|
+
* protocol to be unique within a zone.
|
|
545
|
+
*
|
|
546
|
+
* The order of returned sections is the zone's authored order
|
|
547
|
+
* (`sections()` ordering, spec §2.5.2).
|
|
548
|
+
*
|
|
549
|
+
* @param title Section title to look up — exact, case-sensitive.
|
|
550
|
+
*/
|
|
551
|
+
async findSectionsByTitle(title) {
|
|
552
|
+
const all = await this.#parent._readZone(this.#name);
|
|
553
|
+
return all.filter((s) => s.title === title);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Stage an update for **every** section in this zone whose `title`
|
|
557
|
+
* matches `title` exactly. Returns the list of section IDs that were
|
|
558
|
+
* staged — empty when nothing matched.
|
|
559
|
+
*
|
|
560
|
+
* Apply with `client.publish()` like any other staged mutation. The
|
|
561
|
+
* staged entries are identical to what `updateSection(id, patch)` would
|
|
562
|
+
* produce, one per matched section, so `pendingChanges()` /
|
|
563
|
+
* `discard()` behave normally.
|
|
564
|
+
*
|
|
565
|
+
* Note: this method does NOT throw when there is no match — it returns
|
|
566
|
+
* `[]`. That's intentional: callers driven by an LLM frequently want to
|
|
567
|
+
* upsert (try update, then fall back to add) and shouldn't have to
|
|
568
|
+
* catch.
|
|
569
|
+
*
|
|
570
|
+
* @param title Section title to look up — exact, case-sensitive.
|
|
571
|
+
* @param patch Same patch shape accepted by `updateSection`.
|
|
572
|
+
* @returns Array of `section.id` strings whose updates were staged.
|
|
573
|
+
*/
|
|
574
|
+
async updateSectionsByTitle(title, patch) {
|
|
575
|
+
const matches = await this.findSectionsByTitle(title);
|
|
576
|
+
const ids = [];
|
|
577
|
+
for (const s of matches) {
|
|
578
|
+
this.#parent._stageUpdate(this.#name, s.id, patch);
|
|
579
|
+
ids.push(s.id);
|
|
580
|
+
}
|
|
581
|
+
return ids;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Stage a delete for **every** section in this zone whose `title`
|
|
585
|
+
* matches `title` exactly. Returns the list of section IDs that were
|
|
586
|
+
* staged — empty when nothing matched.
|
|
587
|
+
*
|
|
588
|
+
* Same semantics as {@link updateSectionsByTitle}: silent on no-match,
|
|
589
|
+
* apply with `client.publish()`.
|
|
590
|
+
*
|
|
591
|
+
* @param title Section title to look up — exact, case-sensitive.
|
|
592
|
+
* @returns Array of `section.id` strings whose deletes were staged.
|
|
593
|
+
*/
|
|
594
|
+
async deleteSectionsByTitle(title) {
|
|
595
|
+
const matches = await this.findSectionsByTitle(title);
|
|
596
|
+
const ids = [];
|
|
597
|
+
for (const s of matches) {
|
|
598
|
+
this.#parent._stageDelete(this.#name, s.id);
|
|
599
|
+
ids.push(s.id);
|
|
600
|
+
}
|
|
601
|
+
return ids;
|
|
602
|
+
}
|
|
520
603
|
}
|
|
521
604
|
export class EthosNamespace {
|
|
522
605
|
#deps;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
1
|
+
export declare const VERSION = "0.1.0-alpha.23";
|
|
2
2
|
export { AithosSDK } from "./sdk.js";
|
|
3
3
|
export type { AithosSDKConfig } from "./types.js";
|
|
4
4
|
export { AithosSDKError } from "./types.js";
|
|
@@ -9,6 +9,8 @@ export type { ComputeMessage, ImageAspectRatio, ImageModelId, InvokeBedrockArgs,
|
|
|
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";
|
|
12
|
+
export type { ComponentStyle, ExtractArgs, ExtractContent, ExtractData, ExtractForm, ExtractFormField, ExtractHeading, ExtractIconDeclaration, ExtractImage, ExtractLink, ExtractLogo, ExtractMeta, ExtractResult, ExtractSection, ExtractStructure, ExtractStyles, FetchAssetArgs, FetchAssetResult, PaletteEntry, VisualSignature, WebNamespaceDeps, } from "./web.js";
|
|
13
|
+
export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
|
|
12
14
|
export { AithosAuth, DEFAULT_API_BASE_URL, DEFAULT_AUTH_BASE_URL, } from "./auth.js";
|
|
13
15
|
export type { AithosAuthConfig, AithosSession, CompleteSsoFirstLoginInput, CompleteSsoFirstLoginResult, DelegateInfo, ImportMandateInput, OwnerInfo, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, } from "./auth.js";
|
|
14
16
|
export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, type AithosSessionStore, } from "./session-store.js";
|
package/dist/src/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// Public types specific to the SDK (`AithosSDKConfig`, `AithosSDKError`)
|
|
18
18
|
// are exported from here. Endpoint config (`AithosSdkEndpoints`,
|
|
19
19
|
// `DEFAULT_SDK_ENDPOINTS`) likewise.
|
|
20
|
-
export const VERSION = "0.1.0-alpha.
|
|
20
|
+
export const VERSION = "0.1.0-alpha.23";
|
|
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
|
|
@@ -27,6 +27,7 @@ export { AithosRpcError } from "@aithos/protocol-client";
|
|
|
27
27
|
export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
|
|
28
28
|
export { ComputeNamespace } from "./compute.js";
|
|
29
29
|
export { WalletNamespace } from "./wallet.js";
|
|
30
|
+
export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
|
|
30
31
|
// Sign-up, sign-in, sign-in-with-Google. Lives outside the AithosSDK
|
|
31
32
|
// class because the auth flow runs *before* the user has a
|
|
32
33
|
// BrowserIdentity (sign-up creates one, sign-in restores it from the
|
package/dist/src/sdk.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type AithosSdkEndpoints } from "./endpoints.js";
|
|
|
4
4
|
import { EthosNamespace } from "./ethos.js";
|
|
5
5
|
import { MandatesNamespace } from "./mandates.js";
|
|
6
6
|
import { WalletNamespace } from "./wallet.js";
|
|
7
|
+
import { WebNamespace } from "./web.js";
|
|
7
8
|
export interface AithosSDKConfig {
|
|
8
9
|
/**
|
|
9
10
|
* The {@link AithosAuth} instance the SDK reads sign-in state from.
|
|
@@ -44,6 +45,8 @@ export declare class AithosSDK {
|
|
|
44
45
|
readonly ethos: EthosNamespace;
|
|
45
46
|
/** Mandate lifecycle namespace — create / list / revoke. */
|
|
46
47
|
readonly mandates: MandatesNamespace;
|
|
48
|
+
/** Web extraction namespace — aithos.web_extract through the web extractor proxy. */
|
|
49
|
+
readonly web: WebNamespace;
|
|
47
50
|
constructor(config: AithosSDKConfig);
|
|
48
51
|
/** DID of the currently signed-in owner, or null if no owner is loaded. */
|
|
49
52
|
get userDid(): string | null;
|
package/dist/src/sdk.js
CHANGED
|
@@ -5,6 +5,7 @@ import { resolveEndpoints } from "./endpoints.js";
|
|
|
5
5
|
import { EthosNamespace } from "./ethos.js";
|
|
6
6
|
import { MandatesNamespace } from "./mandates.js";
|
|
7
7
|
import { WalletNamespace } from "./wallet.js";
|
|
8
|
+
import { WebNamespace } from "./web.js";
|
|
8
9
|
export class AithosSDK {
|
|
9
10
|
/** Resolved endpoint configuration (defaults + caller overrides). */
|
|
10
11
|
endpoints;
|
|
@@ -20,6 +21,8 @@ export class AithosSDK {
|
|
|
20
21
|
ethos;
|
|
21
22
|
/** Mandate lifecycle namespace — create / list / revoke. */
|
|
22
23
|
mandates;
|
|
24
|
+
/** Web extraction namespace — aithos.web_extract through the web extractor proxy. */
|
|
25
|
+
web;
|
|
23
26
|
constructor(config) {
|
|
24
27
|
if (!config.auth) {
|
|
25
28
|
throw new TypeError("AithosSDK: config.auth is required");
|
|
@@ -53,6 +56,12 @@ export class AithosSDK {
|
|
|
53
56
|
endpoints: this.endpoints,
|
|
54
57
|
fetch: fetchImpl,
|
|
55
58
|
});
|
|
59
|
+
this.web = new WebNamespace({
|
|
60
|
+
auth: config.auth,
|
|
61
|
+
appDid: config.appDid,
|
|
62
|
+
endpoints: this.endpoints,
|
|
63
|
+
fetch: fetchImpl,
|
|
64
|
+
});
|
|
56
65
|
}
|
|
57
66
|
/** DID of the currently signed-in owner, or null if no owner is loaded. */
|
|
58
67
|
get userDid() {
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { AithosAuth } from "./auth.js";
|
|
2
|
+
import { type AithosSdkEndpoints } from "./endpoints.js";
|
|
3
|
+
/** Opt-in scope a mandate must carry to invoke `aithos.web_extract`. */
|
|
4
|
+
export declare const WEB_EXTRACT_SCOPE: "web.extract";
|
|
5
|
+
export interface ExtractArgs {
|
|
6
|
+
/**
|
|
7
|
+
* Mandate ID under which this call should be attributed.
|
|
8
|
+
*
|
|
9
|
+
* - **Owner sessions**: optional. The SDK uses the owner's own DID
|
|
10
|
+
* as a sentinel "self" mandate id — the proxy skips mandate checks
|
|
11
|
+
* when the envelope is owner-signed.
|
|
12
|
+
* - **Delegate sessions**: required. Must reference the imported
|
|
13
|
+
* mandate bundle the SDK signs with; the proxy enforces the
|
|
14
|
+
* `web.extract` scope.
|
|
15
|
+
*/
|
|
16
|
+
readonly mandateId?: string;
|
|
17
|
+
/** Absolute http(s) URL to extract. */
|
|
18
|
+
readonly url: string;
|
|
19
|
+
/**
|
|
20
|
+
* Playwright `waitUntil` strategy passed straight through to the
|
|
21
|
+
* server-side navigation. Defaults to `"networkidle"` server-side
|
|
22
|
+
* if omitted.
|
|
23
|
+
*/
|
|
24
|
+
readonly waitUntil?: "load" | "domcontentloaded" | "networkidle";
|
|
25
|
+
/** Navigation timeout in ms. Server validates [1000, 60000]. */
|
|
26
|
+
readonly timeoutMs?: number;
|
|
27
|
+
/** Reserved for audit-level deduplication; the proxy currently does not
|
|
28
|
+
* enforce idempotency keys for extractions. */
|
|
29
|
+
readonly idempotencyKey?: string;
|
|
30
|
+
/** Abort signal to cancel the request. */
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
export interface ExtractMeta {
|
|
34
|
+
readonly title: string | null;
|
|
35
|
+
readonly description: string | null;
|
|
36
|
+
readonly lang: string | null;
|
|
37
|
+
readonly charset: string | null;
|
|
38
|
+
readonly viewport: string | null;
|
|
39
|
+
readonly canonical: string | null;
|
|
40
|
+
readonly og: Readonly<Record<string, string>>;
|
|
41
|
+
}
|
|
42
|
+
export interface ExtractHeading {
|
|
43
|
+
readonly level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
44
|
+
readonly text: string;
|
|
45
|
+
readonly id: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface ExtractSection {
|
|
48
|
+
readonly tag: string;
|
|
49
|
+
readonly role: string | null;
|
|
50
|
+
readonly html: string;
|
|
51
|
+
readonly text_len: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ExtractLink {
|
|
54
|
+
readonly label: string;
|
|
55
|
+
readonly href: string;
|
|
56
|
+
readonly internal: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface ExtractImage {
|
|
59
|
+
readonly src: string;
|
|
60
|
+
readonly alt: string | null;
|
|
61
|
+
readonly role: string | null;
|
|
62
|
+
}
|
|
63
|
+
export interface ExtractFormField {
|
|
64
|
+
readonly type: string;
|
|
65
|
+
readonly name: string | null;
|
|
66
|
+
readonly required: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface ExtractForm {
|
|
69
|
+
readonly action: string | null;
|
|
70
|
+
readonly method: string;
|
|
71
|
+
readonly fields: readonly ExtractFormField[];
|
|
72
|
+
}
|
|
73
|
+
export interface ExtractStructure {
|
|
74
|
+
readonly headings: readonly ExtractHeading[];
|
|
75
|
+
readonly sections: readonly ExtractSection[];
|
|
76
|
+
readonly nav_links: readonly ExtractLink[];
|
|
77
|
+
readonly forms: readonly ExtractForm[];
|
|
78
|
+
}
|
|
79
|
+
export interface ExtractContent {
|
|
80
|
+
readonly main_html: string;
|
|
81
|
+
readonly main_text: string;
|
|
82
|
+
readonly images: readonly ExtractImage[];
|
|
83
|
+
readonly links: {
|
|
84
|
+
readonly internal: readonly ExtractLink[];
|
|
85
|
+
readonly external: readonly ExtractLink[];
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export interface ExtractStyles {
|
|
89
|
+
readonly css: string;
|
|
90
|
+
readonly inline_styles_count: number;
|
|
91
|
+
}
|
|
92
|
+
export interface PaletteEntry {
|
|
93
|
+
readonly hex: string;
|
|
94
|
+
readonly weight: number;
|
|
95
|
+
readonly role: "background" | "text" | "accent" | "other";
|
|
96
|
+
}
|
|
97
|
+
export interface ComponentStyle {
|
|
98
|
+
readonly count: number;
|
|
99
|
+
readonly bg: string | null;
|
|
100
|
+
readonly fg: string | null;
|
|
101
|
+
readonly border: string | null;
|
|
102
|
+
readonly radius: string | null;
|
|
103
|
+
readonly padding: string | null;
|
|
104
|
+
readonly font_size: string | null;
|
|
105
|
+
readonly font_weight: string | null;
|
|
106
|
+
}
|
|
107
|
+
export interface VisualSignature {
|
|
108
|
+
readonly colors: {
|
|
109
|
+
readonly palette: readonly PaletteEntry[];
|
|
110
|
+
readonly background: string | null;
|
|
111
|
+
readonly text: string | null;
|
|
112
|
+
readonly primary: string | null;
|
|
113
|
+
readonly link: string | null;
|
|
114
|
+
};
|
|
115
|
+
readonly typography: {
|
|
116
|
+
readonly heading_font: string | null;
|
|
117
|
+
readonly body_font: string | null;
|
|
118
|
+
readonly size_scale: readonly number[];
|
|
119
|
+
readonly base_size_px: number | null;
|
|
120
|
+
readonly base_line_height: number | null;
|
|
121
|
+
};
|
|
122
|
+
readonly radii: {
|
|
123
|
+
readonly button: string | null;
|
|
124
|
+
readonly input: string | null;
|
|
125
|
+
readonly card: string | null;
|
|
126
|
+
};
|
|
127
|
+
readonly spacing: {
|
|
128
|
+
readonly base_unit_px: number | null;
|
|
129
|
+
readonly common_gaps_px: readonly number[];
|
|
130
|
+
};
|
|
131
|
+
readonly layout: {
|
|
132
|
+
readonly max_content_width_px: number | null;
|
|
133
|
+
readonly mode: "flex" | "grid" | "block" | null;
|
|
134
|
+
};
|
|
135
|
+
readonly components: {
|
|
136
|
+
readonly buttons: readonly ComponentStyle[];
|
|
137
|
+
readonly inputs: readonly ComponentStyle[];
|
|
138
|
+
readonly cards: readonly ComponentStyle[];
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export interface ExtractIconDeclaration {
|
|
142
|
+
/** href as written in the HTML (relative or absolute). */
|
|
143
|
+
readonly href: string;
|
|
144
|
+
/** rel value, lowercased: "icon", "apple-touch-icon", "shortcut icon", ... */
|
|
145
|
+
readonly rel: string;
|
|
146
|
+
/** Declared `sizes` attribute, e.g. "180x180" or "any" or null. */
|
|
147
|
+
readonly sizes: string | null;
|
|
148
|
+
/** Declared mime type, e.g. "image/svg+xml" or null. */
|
|
149
|
+
readonly type: string | null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Logo asset resolved server-side. The Lambda picks the best
|
|
153
|
+
* symbol-only asset available on the page — declared <link rel="icon"|
|
|
154
|
+
* "apple-touch-icon"> declarations + conventional well-known paths
|
|
155
|
+
* (/apple-touch-icon.png, /favicon.svg, /favicon.ico) — in that
|
|
156
|
+
* order of expected quality. Null when nothing resolves.
|
|
157
|
+
*
|
|
158
|
+
* Favicons are symbol-only by construction (no designer ships a
|
|
159
|
+
* wordmark inside a 16-180 px icon), which sidesteps the
|
|
160
|
+
* lockup-vs-symbol problem callers used to handle client-side with
|
|
161
|
+
* a vision model.
|
|
162
|
+
*/
|
|
163
|
+
export interface ExtractLogo {
|
|
164
|
+
/** Absolute URL of the asset that was successfully fetched. */
|
|
165
|
+
readonly url: string;
|
|
166
|
+
/** Which source produced the winner. */
|
|
167
|
+
readonly source: "link-icon-svg" | "link-apple-touch-icon" | "link-icon-large" | "link-icon" | "link-shortcut-icon" | "well-known-apple-180" | "well-known-apple" | "well-known-svg" | "well-known-png-large" | "well-known-ico";
|
|
168
|
+
readonly content_type: string;
|
|
169
|
+
readonly size_bytes: number;
|
|
170
|
+
/** Base64-encoded asset bytes (no `data:` prefix). Build a data
|
|
171
|
+
* URI with `data:${content_type};base64,${base64}`. */
|
|
172
|
+
readonly base64: string;
|
|
173
|
+
}
|
|
174
|
+
export interface ExtractData {
|
|
175
|
+
readonly url: string;
|
|
176
|
+
readonly final_url: string;
|
|
177
|
+
readonly fetched_at: string;
|
|
178
|
+
readonly render_ms: number;
|
|
179
|
+
readonly meta: ExtractMeta;
|
|
180
|
+
readonly structure: ExtractStructure;
|
|
181
|
+
readonly content: ExtractContent;
|
|
182
|
+
readonly styles: ExtractStyles;
|
|
183
|
+
readonly visual_signature: VisualSignature;
|
|
184
|
+
/**
|
|
185
|
+
* Best logo asset resolved by the lambda — null when no
|
|
186
|
+
* <link rel="icon"> declaration and no conventional favicon
|
|
187
|
+
* path produced a usable image. Callers should then let the
|
|
188
|
+
* operator upload the logo manually rather than treat this as
|
|
189
|
+
* a fatal error.
|
|
190
|
+
*/
|
|
191
|
+
readonly logo: ExtractLogo | null;
|
|
192
|
+
}
|
|
193
|
+
export interface ExtractResult {
|
|
194
|
+
/** Cleaned extraction payload. */
|
|
195
|
+
readonly data: ExtractData;
|
|
196
|
+
/** Microcredits charged for this call (1 on success, 0 on refunded failures). */
|
|
197
|
+
readonly creditsCharged: number;
|
|
198
|
+
/** Wallet balance after the (possibly refunded) debit. */
|
|
199
|
+
readonly walletBalance: number;
|
|
200
|
+
/** Audit log id for traceability. */
|
|
201
|
+
readonly auditId: string;
|
|
202
|
+
}
|
|
203
|
+
export interface FetchAssetArgs {
|
|
204
|
+
/** Absolute http(s) URL of the asset to fetch. */
|
|
205
|
+
readonly url: string;
|
|
206
|
+
/** Mandate id under which this call should be attributed. */
|
|
207
|
+
readonly mandateId?: string;
|
|
208
|
+
/** Abort signal. */
|
|
209
|
+
readonly signal?: AbortSignal;
|
|
210
|
+
}
|
|
211
|
+
export interface FetchAssetResult {
|
|
212
|
+
/** Asset payload. */
|
|
213
|
+
readonly data: {
|
|
214
|
+
/** URL we asked the proxy to fetch. */
|
|
215
|
+
readonly url: string;
|
|
216
|
+
/** URL after the proxy followed any redirects. */
|
|
217
|
+
readonly final_url: string;
|
|
218
|
+
/** Content-Type reported by the upstream server. */
|
|
219
|
+
readonly content_type: string;
|
|
220
|
+
/** Size of the fetched body in bytes. */
|
|
221
|
+
readonly size_bytes: number;
|
|
222
|
+
/** Base64-encoded body (no `data:` prefix). Build a data URI
|
|
223
|
+
* with `data:${content_type};base64,${base64}`. */
|
|
224
|
+
readonly base64: string;
|
|
225
|
+
};
|
|
226
|
+
/** Microcredits charged for this call. */
|
|
227
|
+
readonly creditsCharged: number;
|
|
228
|
+
readonly walletBalance: number;
|
|
229
|
+
readonly auditId: string;
|
|
230
|
+
}
|
|
231
|
+
export interface WebNamespaceDeps {
|
|
232
|
+
readonly auth: AithosAuth;
|
|
233
|
+
readonly appDid: string;
|
|
234
|
+
readonly endpoints: AithosSdkEndpoints;
|
|
235
|
+
readonly fetch: typeof fetch;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `sdk.web` namespace — Aithos's web extraction primitive.
|
|
239
|
+
*
|
|
240
|
+
* Designed so a downstream agent can read the static content of any
|
|
241
|
+
* public page (HTML, purged CSS, computed visual signature) without
|
|
242
|
+
* involving an LLM — saving ~30× over a Bedrock-based extraction in
|
|
243
|
+
* both latency and cost.
|
|
244
|
+
*
|
|
245
|
+
* @throws {AithosSDKError} — same error taxonomy as `sdk.compute`,
|
|
246
|
+
* including `-32071` (insufficient balance with `{required, available}`
|
|
247
|
+
* in `data`) and `-32042` (mandate scope mismatch).
|
|
248
|
+
*/
|
|
249
|
+
export declare class WebNamespace {
|
|
250
|
+
#private;
|
|
251
|
+
constructor(deps: WebNamespaceDeps);
|
|
252
|
+
/**
|
|
253
|
+
* Extract a public webpage. Returns the cleaned HTML, purged CSS and
|
|
254
|
+
* a deterministic visual signature (palette, typography, dominant
|
|
255
|
+
* radii, spacing, layout mode, component digests).
|
|
256
|
+
*/
|
|
257
|
+
extract(args: ExtractArgs): Promise<ExtractResult>;
|
|
258
|
+
/**
|
|
259
|
+
* Fetch a single asset (image / font / css / json …) server-side,
|
|
260
|
+
* bypassing browser CORS. Returns the bytes as base64 + content-type.
|
|
261
|
+
*
|
|
262
|
+
* Use when `fetch(url, {mode: "cors"})` and `<img crossOrigin>`
|
|
263
|
+
* canvas readback both fail because the asset server doesn't return
|
|
264
|
+
* Access-Control-Allow-Origin headers — typical for production
|
|
265
|
+
* sites' logos hosted on the main domain.
|
|
266
|
+
*
|
|
267
|
+
* For the common "logo of a webpage" case the lambda already
|
|
268
|
+
* resolves and embeds the best symbol-only logo in
|
|
269
|
+
* {@link extract}'s `data.logo` field; you only need fetchAsset
|
|
270
|
+
* when extract's logo doesn't fit, when picking up secondary
|
|
271
|
+
* assets (og:image, hero image, document download), or when
|
|
272
|
+
* fetching an asset on a page you haven't extracted.
|
|
273
|
+
*
|
|
274
|
+
* Costs 1 mc per successful fetch, full refund on failure. Server
|
|
275
|
+
* caps: 15 s timeout, 10 MB body, http/https only.
|
|
276
|
+
*/
|
|
277
|
+
fetchAsset(args: FetchAssetArgs): Promise<FetchAssetResult>;
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=web.d.ts.map
|
package/dist/src/web.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Web namespace — `aithos.web_extract` through the web-extractor proxy.
|
|
4
|
+
//
|
|
5
|
+
// Same JSON-RPC + signed-envelope protocol as the compute namespace, but
|
|
6
|
+
// against a separate Aithos service (`extract.aithos.be`). Pricing is a
|
|
7
|
+
// flat 1 microcredit per successful extraction (refunded on failure).
|
|
8
|
+
//
|
|
9
|
+
// The mandate scope is `web.extract` (exported as {@link WEB_EXTRACT_SCOPE}
|
|
10
|
+
// for owner mint-time use). A delegate that holds only this scope can read
|
|
11
|
+
// pages on the owner's behalf without gaining LLM-spend authority.
|
|
12
|
+
//
|
|
13
|
+
// Signing follows the same owner-vs-delegate logic as the compute namespace
|
|
14
|
+
// (see ComputeNamespace.#resolveSigner). The duplication is bounded — both
|
|
15
|
+
// namespaces' `#signAndPost` helpers can later move into a shared internal
|
|
16
|
+
// once a third primitive arrives.
|
|
17
|
+
import { buildSignedEnvelope, } from "@aithos/protocol-client";
|
|
18
|
+
import { webInvokeUrl, } from "./endpoints.js";
|
|
19
|
+
import { delegateKeyPair, ownerKeyPair, } from "./internal/protocol-client-bridge.js";
|
|
20
|
+
import { AithosSDKError } from "./types.js";
|
|
21
|
+
/** Opt-in scope a mandate must carry to invoke `aithos.web_extract`. */
|
|
22
|
+
export const WEB_EXTRACT_SCOPE = "web.extract";
|
|
23
|
+
/**
|
|
24
|
+
* `sdk.web` namespace — Aithos's web extraction primitive.
|
|
25
|
+
*
|
|
26
|
+
* Designed so a downstream agent can read the static content of any
|
|
27
|
+
* public page (HTML, purged CSS, computed visual signature) without
|
|
28
|
+
* involving an LLM — saving ~30× over a Bedrock-based extraction in
|
|
29
|
+
* both latency and cost.
|
|
30
|
+
*
|
|
31
|
+
* @throws {AithosSDKError} — same error taxonomy as `sdk.compute`,
|
|
32
|
+
* including `-32071` (insufficient balance with `{required, available}`
|
|
33
|
+
* in `data`) and `-32042` (mandate scope mismatch).
|
|
34
|
+
*/
|
|
35
|
+
export class WebNamespace {
|
|
36
|
+
#deps;
|
|
37
|
+
constructor(deps) {
|
|
38
|
+
this.#deps = deps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Extract a public webpage. Returns the cleaned HTML, purged CSS and
|
|
42
|
+
* a deterministic visual signature (palette, typography, dominant
|
|
43
|
+
* radii, spacing, layout mode, component digests).
|
|
44
|
+
*/
|
|
45
|
+
async extract(args) {
|
|
46
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
47
|
+
const choice = this.#resolveSigner(args.mandateId);
|
|
48
|
+
const url = webInvokeUrl(endpoints);
|
|
49
|
+
const params = {
|
|
50
|
+
app_did: this.#deps.appDid,
|
|
51
|
+
mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
|
|
52
|
+
url: args.url,
|
|
53
|
+
};
|
|
54
|
+
if (args.waitUntil !== undefined)
|
|
55
|
+
params.waitUntil = args.waitUntil;
|
|
56
|
+
if (args.timeoutMs !== undefined)
|
|
57
|
+
params.timeoutMs = args.timeoutMs;
|
|
58
|
+
if (args.idempotencyKey !== undefined) {
|
|
59
|
+
params.idempotencyKey = args.idempotencyKey;
|
|
60
|
+
}
|
|
61
|
+
return await this.#signAndPost({
|
|
62
|
+
url,
|
|
63
|
+
method: "aithos.web_extract",
|
|
64
|
+
params,
|
|
65
|
+
choice,
|
|
66
|
+
fetchImpl,
|
|
67
|
+
signal: args.signal,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fetch a single asset (image / font / css / json …) server-side,
|
|
72
|
+
* bypassing browser CORS. Returns the bytes as base64 + content-type.
|
|
73
|
+
*
|
|
74
|
+
* Use when `fetch(url, {mode: "cors"})` and `<img crossOrigin>`
|
|
75
|
+
* canvas readback both fail because the asset server doesn't return
|
|
76
|
+
* Access-Control-Allow-Origin headers — typical for production
|
|
77
|
+
* sites' logos hosted on the main domain.
|
|
78
|
+
*
|
|
79
|
+
* For the common "logo of a webpage" case the lambda already
|
|
80
|
+
* resolves and embeds the best symbol-only logo in
|
|
81
|
+
* {@link extract}'s `data.logo` field; you only need fetchAsset
|
|
82
|
+
* when extract's logo doesn't fit, when picking up secondary
|
|
83
|
+
* assets (og:image, hero image, document download), or when
|
|
84
|
+
* fetching an asset on a page you haven't extracted.
|
|
85
|
+
*
|
|
86
|
+
* Costs 1 mc per successful fetch, full refund on failure. Server
|
|
87
|
+
* caps: 15 s timeout, 10 MB body, http/https only.
|
|
88
|
+
*/
|
|
89
|
+
async fetchAsset(args) {
|
|
90
|
+
const { endpoints, fetch: fetchImpl } = this.#deps;
|
|
91
|
+
const choice = this.#resolveSigner(args.mandateId);
|
|
92
|
+
const url = webInvokeUrl(endpoints);
|
|
93
|
+
const params = {
|
|
94
|
+
app_did: this.#deps.appDid,
|
|
95
|
+
mandate_id: this.#resolveMandateIdForWire(args.mandateId, choice),
|
|
96
|
+
url: args.url,
|
|
97
|
+
};
|
|
98
|
+
return await this.#signAndPost({
|
|
99
|
+
url,
|
|
100
|
+
method: "aithos.web_fetch_asset",
|
|
101
|
+
params,
|
|
102
|
+
choice,
|
|
103
|
+
fetchImpl,
|
|
104
|
+
signal: args.signal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/* ----------------------------- internals ----------------------------- */
|
|
108
|
+
#resolveSigner(mandateId) {
|
|
109
|
+
const { auth } = this.#deps;
|
|
110
|
+
const owner = auth._getOwnerSigners();
|
|
111
|
+
const ownerLoaded = owner !== null && !owner.destroyed;
|
|
112
|
+
if (ownerLoaded) {
|
|
113
|
+
const publicKp = ownerKeyPair(owner, "public");
|
|
114
|
+
return {
|
|
115
|
+
kind: "owner",
|
|
116
|
+
iss: owner.did,
|
|
117
|
+
verificationMethod: `${owner.did}#public`,
|
|
118
|
+
signer: publicKp,
|
|
119
|
+
mandate: undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (mandateId === undefined || mandateId.length === 0) {
|
|
123
|
+
throw new AithosSDKError("sdk_no_signer", "no owner signed in and no mandateId provided — pass a mandateId for a delegate session, or sign in as an owner first.");
|
|
124
|
+
}
|
|
125
|
+
const actor = auth._getDelegateActor(mandateId);
|
|
126
|
+
if (!actor || actor.destroyed) {
|
|
127
|
+
throw new AithosSDKError("sdk_no_delegate_for_mandate", `no owner signed in and no imported delegate mandate matches '${mandateId}'. Sign in as an owner, or import a delegate bundle for that mandate via auth.importMandate.`);
|
|
128
|
+
}
|
|
129
|
+
const kp = delegateKeyPair(actor);
|
|
130
|
+
return {
|
|
131
|
+
kind: "delegate",
|
|
132
|
+
iss: actor.subjectDid,
|
|
133
|
+
verificationMethod: actor.granteePubkeyMultibase,
|
|
134
|
+
signer: kp,
|
|
135
|
+
mandate: actor.mandate,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
#resolveMandateIdForWire(explicit, choice) {
|
|
139
|
+
if (explicit && explicit.length > 0)
|
|
140
|
+
return explicit;
|
|
141
|
+
if (choice.kind === "delegate")
|
|
142
|
+
return choice.mandate.id;
|
|
143
|
+
return `${choice.iss}#self`;
|
|
144
|
+
}
|
|
145
|
+
async #signAndPost(opts) {
|
|
146
|
+
const { url, method, params, choice, fetchImpl, signal } = opts;
|
|
147
|
+
const envelope = buildSignedEnvelope({
|
|
148
|
+
iss: choice.iss,
|
|
149
|
+
aud: url,
|
|
150
|
+
method,
|
|
151
|
+
verificationMethod: choice.verificationMethod,
|
|
152
|
+
params,
|
|
153
|
+
signer: choice.signer,
|
|
154
|
+
...(choice.kind === "delegate" ? { mandate: choice.mandate } : {}),
|
|
155
|
+
});
|
|
156
|
+
let res;
|
|
157
|
+
try {
|
|
158
|
+
res = await fetchImpl(url, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "content-type": "application/json" },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
jsonrpc: "2.0",
|
|
163
|
+
id: method,
|
|
164
|
+
method,
|
|
165
|
+
params: { ...params, _envelope: envelope },
|
|
166
|
+
}),
|
|
167
|
+
...(signal ? { signal } : {}),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
throw new AithosSDKError("network", e.message);
|
|
172
|
+
}
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
throw new AithosSDKError("http", `HTTP ${res.status} ${res.statusText}`, { status: res.status });
|
|
175
|
+
}
|
|
176
|
+
const body = (await res.json());
|
|
177
|
+
if (body.error) {
|
|
178
|
+
throw new AithosSDKError(String(body.error.code), body.error.message, body.error.data ? { data: body.error.data } : undefined);
|
|
179
|
+
}
|
|
180
|
+
if (!body.result) {
|
|
181
|
+
throw new AithosSDKError("empty", "empty result from web extractor proxy");
|
|
182
|
+
}
|
|
183
|
+
return body.result;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { strict as assert } from "node:assert";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import { DEFAULT_SDK_ENDPOINTS } from "../src/index.js";
|
|
7
|
-
import { computeInvokeUrl, resolveEndpoints, walletTopupCheckoutUrl, } from "../src/endpoints.js";
|
|
7
|
+
import { computeInvokeUrl, resolveEndpoints, walletTopupCheckoutUrl, webInvokeUrl, } from "../src/endpoints.js";
|
|
8
8
|
describe("resolveEndpoints", () => {
|
|
9
9
|
it("returns a fresh copy of the defaults when no override is given", () => {
|
|
10
10
|
const a = resolveEndpoints();
|
|
@@ -23,12 +23,14 @@ describe("computeInvokeUrl", () => {
|
|
|
23
23
|
assert.equal(computeInvokeUrl({
|
|
24
24
|
compute: "https://compute.aithos.be",
|
|
25
25
|
wallet: "https://wallet.aithos.be",
|
|
26
|
+
web: "https://extract.aithos.be",
|
|
26
27
|
}), "https://compute.aithos.be/v1/invoke");
|
|
27
28
|
});
|
|
28
29
|
it("trims a trailing slash on the compute base", () => {
|
|
29
30
|
assert.equal(computeInvokeUrl({
|
|
30
31
|
compute: "https://compute.aithos.be/",
|
|
31
32
|
wallet: "https://wallet.aithos.be",
|
|
33
|
+
web: "https://extract.aithos.be",
|
|
32
34
|
}), "https://compute.aithos.be/v1/invoke");
|
|
33
35
|
});
|
|
34
36
|
});
|
|
@@ -37,7 +39,24 @@ describe("walletTopupCheckoutUrl", () => {
|
|
|
37
39
|
assert.equal(walletTopupCheckoutUrl({
|
|
38
40
|
compute: "https://compute.aithos.be",
|
|
39
41
|
wallet: "https://wallet.aithos.be",
|
|
42
|
+
web: "https://extract.aithos.be",
|
|
40
43
|
}), "https://wallet.aithos.be/v1/wallet/topup/checkout");
|
|
41
44
|
});
|
|
42
45
|
});
|
|
46
|
+
describe("webInvokeUrl", () => {
|
|
47
|
+
it("appends /v1/invoke to the web base", () => {
|
|
48
|
+
assert.equal(webInvokeUrl({
|
|
49
|
+
compute: "https://compute.aithos.be",
|
|
50
|
+
wallet: "https://wallet.aithos.be",
|
|
51
|
+
web: "https://extract.aithos.be",
|
|
52
|
+
}), "https://extract.aithos.be/v1/invoke");
|
|
53
|
+
});
|
|
54
|
+
it("trims a trailing slash on the web base", () => {
|
|
55
|
+
assert.equal(webInvokeUrl({
|
|
56
|
+
compute: "https://compute.aithos.be",
|
|
57
|
+
wallet: "https://wallet.aithos.be",
|
|
58
|
+
web: "https://extract.aithos.be/",
|
|
59
|
+
}), "https://extract.aithos.be/v1/invoke");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
43
62
|
//# sourceMappingURL=endpoints.test.js.map
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Unit tests for sdk.web.extract with a mock fetch.
|
|
4
|
+
//
|
|
5
|
+
// Mirrors test/compute.test.ts: real BrowserIdentity (synchronous Ed25519)
|
|
6
|
+
// so the envelope-signing path runs for real and any
|
|
7
|
+
// signature/canonicalization regression surfaces here.
|
|
8
|
+
import { strict as assert } from "node:assert";
|
|
9
|
+
import { describe, it } from "node:test";
|
|
10
|
+
import { createBrowserIdentity } from "@aithos/protocol-client";
|
|
11
|
+
import { AithosAuth, AithosSDK, AithosSDKError, memoryKeyStore, noopStore, } from "../src/index.js";
|
|
12
|
+
import { serializeRecoveryFile } from "../src/internal/recovery-file.js";
|
|
13
|
+
const APP_DID = "did:aithos:app:test";
|
|
14
|
+
async function makeSdk(fetchImpl) {
|
|
15
|
+
const id = createBrowserIdentity("test-handle", "Test User");
|
|
16
|
+
const auth = new AithosAuth({
|
|
17
|
+
authBaseUrl: "https://auth.test",
|
|
18
|
+
fetch: (() => {
|
|
19
|
+
throw new Error("auth not used in web tests");
|
|
20
|
+
}),
|
|
21
|
+
sessionStore: noopStore(),
|
|
22
|
+
keyStore: memoryKeyStore(),
|
|
23
|
+
});
|
|
24
|
+
const { text } = serializeRecoveryFile(id);
|
|
25
|
+
await auth.signInWithRecovery({ file: text });
|
|
26
|
+
return new AithosSDK({
|
|
27
|
+
auth,
|
|
28
|
+
appDid: APP_DID,
|
|
29
|
+
endpoints: { web: "https://extract.example.test" },
|
|
30
|
+
fetch: fetchImpl,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const HAPPY_RESULT = {
|
|
34
|
+
data: {
|
|
35
|
+
url: "https://example.com",
|
|
36
|
+
final_url: "https://example.com/",
|
|
37
|
+
fetched_at: "2026-05-13T05:30:00.000Z",
|
|
38
|
+
render_ms: 1234,
|
|
39
|
+
meta: {
|
|
40
|
+
title: "Example",
|
|
41
|
+
description: null,
|
|
42
|
+
lang: "en",
|
|
43
|
+
charset: "UTF-8",
|
|
44
|
+
viewport: null,
|
|
45
|
+
canonical: null,
|
|
46
|
+
og: {},
|
|
47
|
+
},
|
|
48
|
+
structure: { headings: [], sections: [], nav_links: [], forms: [] },
|
|
49
|
+
content: {
|
|
50
|
+
main_html: "<main>hi</main>",
|
|
51
|
+
main_text: "hi",
|
|
52
|
+
images: [],
|
|
53
|
+
links: { internal: [], external: [] },
|
|
54
|
+
},
|
|
55
|
+
styles: { css: "", inline_styles_count: 0 },
|
|
56
|
+
visual_signature: {
|
|
57
|
+
colors: {
|
|
58
|
+
palette: [],
|
|
59
|
+
background: "#ffffff",
|
|
60
|
+
text: "#000000",
|
|
61
|
+
primary: null,
|
|
62
|
+
link: null,
|
|
63
|
+
},
|
|
64
|
+
typography: {
|
|
65
|
+
heading_font: null,
|
|
66
|
+
body_font: "sans-serif",
|
|
67
|
+
size_scale: [],
|
|
68
|
+
base_size_px: 16,
|
|
69
|
+
base_line_height: 1.5,
|
|
70
|
+
},
|
|
71
|
+
radii: { button: null, input: null, card: null },
|
|
72
|
+
spacing: { base_unit_px: null, common_gaps_px: [] },
|
|
73
|
+
layout: { max_content_width_px: null, mode: "block" },
|
|
74
|
+
components: { buttons: [], inputs: [], cards: [] },
|
|
75
|
+
},
|
|
76
|
+
logo: null,
|
|
77
|
+
},
|
|
78
|
+
creditsCharged: 1,
|
|
79
|
+
walletBalance: 99_999,
|
|
80
|
+
auditId: "audit-web-1",
|
|
81
|
+
};
|
|
82
|
+
describe("web.extract — happy path", () => {
|
|
83
|
+
it("posts to ${web}/v1/invoke with a JSON-RPC envelope and parses the result", async () => {
|
|
84
|
+
let capturedUrl;
|
|
85
|
+
let capturedInit;
|
|
86
|
+
const fakeFetch = async (input, init) => {
|
|
87
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
88
|
+
capturedInit = init;
|
|
89
|
+
return new Response(JSON.stringify({ result: HAPPY_RESULT }), {
|
|
90
|
+
status: 200,
|
|
91
|
+
headers: { "content-type": "application/json" },
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const sdk = await makeSdk(fakeFetch);
|
|
95
|
+
const out = await sdk.web.extract({ url: "https://example.com" });
|
|
96
|
+
assert.deepEqual(out, HAPPY_RESULT);
|
|
97
|
+
assert.equal(capturedUrl, "https://extract.example.test/v1/invoke");
|
|
98
|
+
assert.equal(capturedInit?.method, "POST");
|
|
99
|
+
const headers = capturedInit?.headers;
|
|
100
|
+
assert.equal(headers["content-type"], "application/json");
|
|
101
|
+
const body = JSON.parse(capturedInit?.body);
|
|
102
|
+
assert.equal(body.jsonrpc, "2.0");
|
|
103
|
+
assert.equal(body.method, "aithos.web_extract");
|
|
104
|
+
assert.equal(body.params.url, "https://example.com");
|
|
105
|
+
assert.equal(body.params.app_did, APP_DID);
|
|
106
|
+
assert.ok(typeof body.params.mandate_id === "string");
|
|
107
|
+
assert.ok(body.params.mandate_id.length > 0);
|
|
108
|
+
assert.ok(body.params._envelope, "request must carry a signed envelope");
|
|
109
|
+
});
|
|
110
|
+
it("forwards waitUntil and timeoutMs as snake_case-free params", async () => {
|
|
111
|
+
let capturedBody;
|
|
112
|
+
const fakeFetch = async (_input, init) => {
|
|
113
|
+
capturedBody = JSON.parse(init.body);
|
|
114
|
+
return new Response(JSON.stringify({ result: HAPPY_RESULT }), {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: { "content-type": "application/json" },
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const sdk = await makeSdk(fakeFetch);
|
|
120
|
+
await sdk.web.extract({
|
|
121
|
+
url: "https://example.com",
|
|
122
|
+
waitUntil: "domcontentloaded",
|
|
123
|
+
timeoutMs: 5000,
|
|
124
|
+
});
|
|
125
|
+
assert.equal(capturedBody?.params.waitUntil, "domcontentloaded");
|
|
126
|
+
assert.equal(capturedBody?.params.timeoutMs, 5000);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("web.extract — error mapping", () => {
|
|
130
|
+
it("propagates -32071 insufficient_balance as AithosSDKError with data", async () => {
|
|
131
|
+
const fakeFetch = async () => new Response(JSON.stringify({
|
|
132
|
+
error: {
|
|
133
|
+
code: -32071,
|
|
134
|
+
message: "Insufficient balance",
|
|
135
|
+
data: { required: 1, available: 0 },
|
|
136
|
+
},
|
|
137
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
138
|
+
const sdk = await makeSdk(fakeFetch);
|
|
139
|
+
await assert.rejects(() => sdk.web.extract({ url: "https://example.com" }), (err) => {
|
|
140
|
+
assert.ok(err instanceof AithosSDKError);
|
|
141
|
+
assert.equal(err.code, "-32071");
|
|
142
|
+
const data = err.data;
|
|
143
|
+
assert.equal(data?.required, 1);
|
|
144
|
+
assert.equal(data?.available, 0);
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
it("propagates -32042 scope mismatch as AithosSDKError", async () => {
|
|
149
|
+
const fakeFetch = async () => new Response(JSON.stringify({
|
|
150
|
+
error: {
|
|
151
|
+
code: -32042,
|
|
152
|
+
message: "mandate does not carry the web.extract scope",
|
|
153
|
+
data: {
|
|
154
|
+
mandate_id: "mandate:abc",
|
|
155
|
+
required_scope: "web.extract",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
159
|
+
const sdk = await makeSdk(fakeFetch);
|
|
160
|
+
await assert.rejects(() => sdk.web.extract({ url: "https://example.com" }), (err) => {
|
|
161
|
+
assert.ok(err instanceof AithosSDKError);
|
|
162
|
+
assert.equal(err.code, "-32042");
|
|
163
|
+
return true;
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it("rejects HTTP transport errors as AithosSDKError code=http", async () => {
|
|
167
|
+
const fakeFetch = async () => new Response("upstream is down", { status: 502 });
|
|
168
|
+
const sdk = await makeSdk(fakeFetch);
|
|
169
|
+
await assert.rejects(() => sdk.web.extract({ url: "https://example.com" }), (err) => {
|
|
170
|
+
assert.ok(err instanceof AithosSDKError);
|
|
171
|
+
assert.equal(err.code, "http");
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe("web.extract — endpoint default", () => {
|
|
177
|
+
it("defaults to https://extract.aithos.be when no override is given", async () => {
|
|
178
|
+
let capturedUrl;
|
|
179
|
+
const fakeFetch = async (input) => {
|
|
180
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
181
|
+
return new Response(JSON.stringify({ result: HAPPY_RESULT }), {
|
|
182
|
+
status: 200,
|
|
183
|
+
headers: { "content-type": "application/json" },
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
const id = createBrowserIdentity("test-handle", "Test User");
|
|
187
|
+
const auth = new AithosAuth({
|
|
188
|
+
authBaseUrl: "https://auth.test",
|
|
189
|
+
fetch: (() => {
|
|
190
|
+
throw new Error("auth not used");
|
|
191
|
+
}),
|
|
192
|
+
sessionStore: noopStore(),
|
|
193
|
+
keyStore: memoryKeyStore(),
|
|
194
|
+
});
|
|
195
|
+
const { text } = serializeRecoveryFile(id);
|
|
196
|
+
await auth.signInWithRecovery({ file: text });
|
|
197
|
+
const sdk = new AithosSDK({
|
|
198
|
+
auth,
|
|
199
|
+
appDid: APP_DID,
|
|
200
|
+
fetch: fakeFetch,
|
|
201
|
+
});
|
|
202
|
+
await sdk.web.extract({ url: "https://example.com" });
|
|
203
|
+
assert.equal(capturedUrl, "https://extract.aithos.be/v1/invoke");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe("web.fetchAsset — happy path", () => {
|
|
207
|
+
it("posts to ${web}/v1/invoke with method aithos.web_fetch_asset", async () => {
|
|
208
|
+
let capturedUrl;
|
|
209
|
+
let capturedInit;
|
|
210
|
+
const fakeResult = {
|
|
211
|
+
data: {
|
|
212
|
+
url: "https://example.com/logo.png",
|
|
213
|
+
final_url: "https://example.com/logo.png",
|
|
214
|
+
content_type: "image/png",
|
|
215
|
+
size_bytes: 4096,
|
|
216
|
+
base64: "aGVsbG8K",
|
|
217
|
+
},
|
|
218
|
+
creditsCharged: 1,
|
|
219
|
+
walletBalance: 99_998,
|
|
220
|
+
auditId: "audit-asset-1",
|
|
221
|
+
};
|
|
222
|
+
const fakeFetch = async (input, init) => {
|
|
223
|
+
capturedUrl = typeof input === "string" ? input : input.toString();
|
|
224
|
+
capturedInit = init;
|
|
225
|
+
return new Response(JSON.stringify({ result: fakeResult }), {
|
|
226
|
+
status: 200,
|
|
227
|
+
headers: { "content-type": "application/json" },
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
const sdk = await makeSdk(fakeFetch);
|
|
231
|
+
const out = await sdk.web.fetchAsset({ url: "https://example.com/logo.png" });
|
|
232
|
+
assert.deepEqual(out, fakeResult);
|
|
233
|
+
assert.equal(capturedUrl, "https://extract.example.test/v1/invoke");
|
|
234
|
+
const body = JSON.parse(capturedInit?.body);
|
|
235
|
+
assert.equal(body.method, "aithos.web_fetch_asset");
|
|
236
|
+
assert.equal(body.params.url, "https://example.com/logo.png");
|
|
237
|
+
assert.equal(body.params.app_did, APP_DID);
|
|
238
|
+
assert.ok(typeof body.params.mandate_id === "string");
|
|
239
|
+
assert.ok(body.params._envelope, "request must carry a signed envelope");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe("web.fetchAsset — error propagation", () => {
|
|
243
|
+
it("propagates -32071 insufficient_balance as AithosSDKError", async () => {
|
|
244
|
+
const fakeFetch = async () => new Response(JSON.stringify({
|
|
245
|
+
error: {
|
|
246
|
+
code: -32071,
|
|
247
|
+
message: "Insufficient balance",
|
|
248
|
+
data: { required: 1, available: 0 },
|
|
249
|
+
},
|
|
250
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
251
|
+
const sdk = await makeSdk(fakeFetch);
|
|
252
|
+
await assert.rejects(() => sdk.web.fetchAsset({ url: "https://example.com/logo.png" }), (err) => {
|
|
253
|
+
assert.ok(err instanceof AithosSDKError);
|
|
254
|
+
assert.equal(err.code, "-32071");
|
|
255
|
+
return true;
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
it("propagates -32000 upstream fetch failure as AithosSDKError", async () => {
|
|
259
|
+
const fakeFetch = async () => new Response(JSON.stringify({
|
|
260
|
+
error: { code: -32000, message: "Asset fetch failed", data: { reason: "upstream HTTP 404" } },
|
|
261
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
262
|
+
const sdk = await makeSdk(fakeFetch);
|
|
263
|
+
await assert.rejects(() => sdk.web.fetchAsset({ url: "https://example.com/missing.png" }), (err) => {
|
|
264
|
+
assert.ok(err instanceof AithosSDKError);
|
|
265
|
+
assert.equal(err.code, "-32000");
|
|
266
|
+
return true;
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
//# sourceMappingURL=web.test.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aithos/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Aithos SDK
|
|
3
|
+
"version": "0.1.0-alpha.23",
|
|
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",
|
|
7
7
|
"sdk",
|