@aithos/sdk 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +45 -0
- package/dist/src/auth-api.d.ts +41 -0
- package/dist/src/auth-api.js +82 -0
- package/dist/src/auth.d.ts +190 -0
- package/dist/src/auth.js +741 -0
- package/dist/src/compute.d.ts +27 -6
- package/dist/src/compute.js +67 -10
- package/dist/src/ethos.d.ts +117 -1
- package/dist/src/ethos.js +646 -16
- package/dist/src/index.d.ts +11 -4
- package/dist/src/index.js +31 -5
- package/dist/src/internal/delegate-bundle.d.ts +18 -0
- package/dist/src/internal/delegate-bundle.js +94 -0
- package/dist/src/internal/delegate-state.d.ts +45 -0
- package/dist/src/internal/delegate-state.js +120 -0
- package/dist/src/internal/owner-signers.d.ts +78 -0
- package/dist/src/internal/owner-signers.js +179 -0
- package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
- package/dist/src/internal/protocol-client-bridge.js +20 -0
- package/dist/src/internal/recovery-file.d.ts +29 -0
- package/dist/src/internal/recovery-file.js +98 -0
- package/dist/src/internal/signer.d.ts +59 -0
- package/dist/src/internal/signer.js +86 -0
- package/dist/src/key-store.d.ts +128 -0
- package/dist/src/key-store.js +244 -0
- package/dist/src/mandates.d.ts +163 -1
- package/dist/src/mandates.js +286 -8
- package/dist/src/sdk.d.ts +36 -3
- package/dist/src/sdk.js +28 -22
- package/dist/src/session-store.d.ts +58 -0
- package/dist/src/session-store.js +158 -0
- package/dist/src/wallet.d.ts +42 -2
- package/dist/src/wallet.js +89 -14
- package/dist/test/auth-j3.test.d.ts +2 -0
- package/dist/test/auth-j3.test.js +391 -0
- package/dist/test/auth.test.d.ts +2 -0
- package/dist/test/auth.test.js +175 -0
- package/dist/test/compute-delegate-path.test.d.ts +2 -0
- package/dist/test/compute-delegate-path.test.js +183 -0
- package/dist/test/compute.test.js +22 -11
- package/dist/test/ethos-first-edition.test.d.ts +2 -0
- package/dist/test/ethos-first-edition.test.js +248 -0
- package/dist/test/ethos.test.d.ts +2 -0
- package/dist/test/ethos.test.js +219 -0
- package/dist/test/key-store.test.d.ts +2 -0
- package/dist/test/key-store.test.js +161 -0
- package/dist/test/mandates-compute.test.d.ts +2 -0
- package/dist/test/mandates-compute.test.js +256 -0
- package/dist/test/mandates.test.d.ts +2 -0
- package/dist/test/mandates.test.js +93 -0
- package/dist/test/sdk.test.js +70 -30
- package/dist/test/signer.test.d.ts +2 -0
- package/dist/test/signer.test.js +117 -0
- package/dist/test/wallet.test.js +20 -9
- package/package.json +4 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* Storage key & expiration */
|
|
5
|
+
/* -------------------------------------------------------------------------- */
|
|
6
|
+
/**
|
|
7
|
+
* Storage key used by the bundled stores. Apps that want to coexist with
|
|
8
|
+
* other Aithos-aware libs (or that want to scope sessions per-tenant) can
|
|
9
|
+
* pass a custom key via {@link sessionStorageStore} or
|
|
10
|
+
* {@link localStorageStore}.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_SESSION_STORAGE_KEY = "aithos.session.v1";
|
|
13
|
+
/** Conservative buffer — drop the session 30 s before its `exp` so we
|
|
14
|
+
* don't hand out a token the server is about to reject. */
|
|
15
|
+
const SESSION_EXPIRY_BUFFER_S = 30;
|
|
16
|
+
function isExpired(session, nowSec) {
|
|
17
|
+
return session.exp <= nowSec + SESSION_EXPIRY_BUFFER_S;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate at runtime that an opaque object looks like an `AithosSession`.
|
|
21
|
+
* Storage values come from JSON.parse over user-controlled data — we can't
|
|
22
|
+
* trust them blind. This isn't a security check (the server validates the
|
|
23
|
+
* JWT ; persistence layers don't authenticate themselves) ; it just
|
|
24
|
+
* prevents weird crashes when the storage was tampered with.
|
|
25
|
+
*/
|
|
26
|
+
function isSessionShaped(v) {
|
|
27
|
+
if (typeof v !== "object" || v === null)
|
|
28
|
+
return false;
|
|
29
|
+
const o = v;
|
|
30
|
+
return (typeof o["session"] === "string" &&
|
|
31
|
+
typeof o["exp"] === "number" &&
|
|
32
|
+
typeof o["did"] === "string" &&
|
|
33
|
+
typeof o["handle"] === "string");
|
|
34
|
+
}
|
|
35
|
+
function browserStorageStore(storageRef, opts = {}) {
|
|
36
|
+
const key = opts.key ?? DEFAULT_SESSION_STORAGE_KEY;
|
|
37
|
+
return {
|
|
38
|
+
get() {
|
|
39
|
+
const s = storageRef();
|
|
40
|
+
if (!s)
|
|
41
|
+
return null;
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = s.getItem(key);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!raw)
|
|
50
|
+
return null;
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Corrupted entry — wipe to recover.
|
|
57
|
+
try {
|
|
58
|
+
s.removeItem(key);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!isSessionShaped(parsed))
|
|
66
|
+
return null;
|
|
67
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
68
|
+
if (isExpired(parsed, nowSec)) {
|
|
69
|
+
// Auto-evict — let the caller see "no session" and re-auth.
|
|
70
|
+
try {
|
|
71
|
+
s.removeItem(key);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
},
|
|
80
|
+
set(session) {
|
|
81
|
+
const s = storageRef();
|
|
82
|
+
if (!s)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
s.setItem(key, JSON.stringify(session));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Quota exceeded, private mode, etc. — log but don't throw : the
|
|
89
|
+
// sign-in returned successfully, the in-memory session is still
|
|
90
|
+
// usable for this tab.
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.warn("[AithosAuth] failed to persist session:", e.message);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
clear() {
|
|
96
|
+
const s = storageRef();
|
|
97
|
+
if (!s)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
s.removeItem(key);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* ignore */
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function safeStorage(getter) {
|
|
109
|
+
return () => {
|
|
110
|
+
try {
|
|
111
|
+
const s = getter();
|
|
112
|
+
return s ?? null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Some restricted contexts (sandboxed iframes, file:// URLs) throw
|
|
116
|
+
// on access. Treat them as "no storage available".
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Default web store : `sessionStorage`. The session lives until the tab
|
|
123
|
+
* is closed. Cleared on `signOut()`. Use this when reauthenticating each
|
|
124
|
+
* day is acceptable and reduces blast radius after an XSS.
|
|
125
|
+
*/
|
|
126
|
+
export function sessionStorageStore(opts) {
|
|
127
|
+
return browserStorageStore(safeStorage(() => (typeof sessionStorage !== "undefined" ? sessionStorage : undefined)), opts);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `localStorage` store. The session persists until the JWT expires or the
|
|
131
|
+
* user explicitly signs out. Higher convenience, larger XSS blast radius.
|
|
132
|
+
*/
|
|
133
|
+
export function localStorageStore(opts) {
|
|
134
|
+
return browserStorageStore(safeStorage(() => (typeof localStorage !== "undefined" ? localStorage : undefined)), opts);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* No-op store. `set` and `clear` discard their input ; `get` always
|
|
138
|
+
* returns null. The default in non-browser contexts (Node, edge runtimes)
|
|
139
|
+
* — apps running there should pass their own store explicitly.
|
|
140
|
+
*/
|
|
141
|
+
export function noopStore() {
|
|
142
|
+
return {
|
|
143
|
+
get: () => null,
|
|
144
|
+
set: () => { },
|
|
145
|
+
clear: () => { },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Pick a sensible default : `sessionStorage` if the browser environment
|
|
150
|
+
* is available, {@link noopStore} otherwise.
|
|
151
|
+
*/
|
|
152
|
+
export function defaultSessionStore() {
|
|
153
|
+
if (typeof sessionStorage !== "undefined") {
|
|
154
|
+
return sessionStorageStore();
|
|
155
|
+
}
|
|
156
|
+
return noopStore();
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=session-store.js.map
|
package/dist/src/wallet.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AithosAuth } from "./auth.js";
|
|
1
2
|
import { type AithosSdkEndpoints } from "./endpoints.js";
|
|
2
3
|
/**
|
|
3
4
|
* Canonical credit-pack identifiers. Pricing and microcredit amounts are
|
|
@@ -24,8 +25,29 @@ export interface CreateTopupSessionResult {
|
|
|
24
25
|
/** Stripe Checkout session id (for diagnostics). */
|
|
25
26
|
readonly sessionId: string;
|
|
26
27
|
}
|
|
28
|
+
export interface GetBalanceArgs {
|
|
29
|
+
/** Abort signal to cancel the request. */
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
export interface GetBalanceResult {
|
|
33
|
+
/** Current wallet balance in microcredits. 0 if the wallet does not yet exist. */
|
|
34
|
+
readonly balance: number;
|
|
35
|
+
/**
|
|
36
|
+
* Microcredits spent today. Resets server-side at midnight UTC.
|
|
37
|
+
* Useful for showing "X / daily cap" UI.
|
|
38
|
+
*/
|
|
39
|
+
readonly dailySpent: number;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a wallet row exists for this DID. False on a never-funded
|
|
42
|
+
* identity — `balance` will be 0 in that case.
|
|
43
|
+
*/
|
|
44
|
+
readonly exists: boolean;
|
|
45
|
+
}
|
|
27
46
|
export interface WalletNamespaceDeps {
|
|
28
|
-
|
|
47
|
+
/** Auth instance — the wallet reads the active owner DID + signing key from here. */
|
|
48
|
+
readonly auth: AithosAuth;
|
|
49
|
+
/** App DID — sent as audit attribution alongside the balance request. */
|
|
50
|
+
readonly appDid: string;
|
|
29
51
|
readonly endpoints: AithosSdkEndpoints;
|
|
30
52
|
readonly fetch: typeof fetch;
|
|
31
53
|
}
|
|
@@ -40,10 +62,28 @@ export declare class WalletNamespace {
|
|
|
40
62
|
* hosted URL — the caller is responsible for redirecting the user (e.g.
|
|
41
63
|
* `window.location.href = result.checkoutUrl`).
|
|
42
64
|
*
|
|
43
|
-
* On success, the Stripe webhook will credit
|
|
65
|
+
* On success, the Stripe webhook will credit the user's wallet once the
|
|
44
66
|
* payment clears. Wallet balances are shared across all Aithos apps that
|
|
45
67
|
* use the same DID.
|
|
46
68
|
*/
|
|
47
69
|
createTopupSession(args: CreateTopupSessionArgs): Promise<CreateTopupSessionResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Read the user's current wallet balance.
|
|
72
|
+
*
|
|
73
|
+
* Routed through the compute proxy at `${compute}/v1/invoke` because
|
|
74
|
+
* that's where the Aithos-platform DDB IAM lives — keeps the wallet
|
|
75
|
+
* Lambda focused on Stripe and avoids granting read-on-wallet to a
|
|
76
|
+
* second function. The call is gated by the same signed-envelope
|
|
77
|
+
* verification as `invokeBedrock`: only the owner of the user's
|
|
78
|
+
* `#public` key can read their balance.
|
|
79
|
+
*
|
|
80
|
+
* Returns `{ balance: 0, exists: false, dailySpent: 0 }` for an
|
|
81
|
+
* identity that has never been funded — the call still succeeds,
|
|
82
|
+
* the row just doesn't exist yet.
|
|
83
|
+
*
|
|
84
|
+
* @throws {AithosSDKError} on protocol errors (network, http,
|
|
85
|
+
* envelope-verify failures from the proxy).
|
|
86
|
+
*/
|
|
87
|
+
getBalance(args?: GetBalanceArgs): Promise<GetBalanceResult>;
|
|
48
88
|
}
|
|
49
89
|
//# sourceMappingURL=wallet.d.ts.map
|
package/dist/src/wallet.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright 2026 Mathieu Colla
|
|
3
|
-
// Wallet namespace —
|
|
3
|
+
// Wallet namespace — credit-pack top-ups (Stripe) and balance lookup.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// v0
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
5
|
+
// Two methods:
|
|
6
|
+
// - createTopupSession() — posts to `${wallet}/v1/wallet/topup/checkout`
|
|
7
|
+
// to create a Stripe Checkout session. The Lambda binds the session
|
|
8
|
+
// to the user's DID via metadata; the Stripe webhook credits the
|
|
9
|
+
// wallet on `checkout.session.completed`. No envelope verify on this
|
|
10
|
+
// endpoint at v0 (the link is single-use and metadata-bound — anyone
|
|
11
|
+
// creating a session for someone else's DID just gifts them credits).
|
|
12
|
+
// - getBalance() — JSON-RPC `aithos.wallet_get_balance` against the
|
|
13
|
+
// compute proxy at `${compute}/v1/invoke`. Read-only DDB GetItem on
|
|
14
|
+
// the wallet table, gated by the same signed envelope verification
|
|
15
|
+
// as compute_invoke. Returns the current balance + daily spent.
|
|
16
|
+
import { buildSignedEnvelope } from "@aithos/protocol-client";
|
|
17
|
+
import { computeInvokeUrl, walletTopupCheckoutUrl, } from "./endpoints.js";
|
|
18
|
+
import { ownerKeyPair } from "./internal/protocol-client-bridge.js";
|
|
15
19
|
import { AithosSDKError } from "./types.js";
|
|
16
20
|
/**
|
|
17
21
|
* `sdk.wallet` namespace.
|
|
@@ -26,12 +30,16 @@ export class WalletNamespace {
|
|
|
26
30
|
* hosted URL — the caller is responsible for redirecting the user (e.g.
|
|
27
31
|
* `window.location.href = result.checkoutUrl`).
|
|
28
32
|
*
|
|
29
|
-
* On success, the Stripe webhook will credit
|
|
33
|
+
* On success, the Stripe webhook will credit the user's wallet once the
|
|
30
34
|
* payment clears. Wallet balances are shared across all Aithos apps that
|
|
31
35
|
* use the same DID.
|
|
32
36
|
*/
|
|
33
37
|
async createTopupSession(args) {
|
|
34
|
-
const {
|
|
38
|
+
const { auth, endpoints, fetch: fetchImpl } = this.#deps;
|
|
39
|
+
const owner = auth._getOwnerSigners();
|
|
40
|
+
if (!owner || owner.destroyed) {
|
|
41
|
+
throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in first");
|
|
42
|
+
}
|
|
35
43
|
const url = walletTopupCheckoutUrl(endpoints);
|
|
36
44
|
let res;
|
|
37
45
|
try {
|
|
@@ -39,7 +47,7 @@ export class WalletNamespace {
|
|
|
39
47
|
method: "POST",
|
|
40
48
|
headers: { "content-type": "application/json" },
|
|
41
49
|
body: JSON.stringify({
|
|
42
|
-
user_did:
|
|
50
|
+
user_did: owner.did,
|
|
43
51
|
pack_id: args.packId,
|
|
44
52
|
success_url: args.successUrl,
|
|
45
53
|
cancel_url: args.cancelUrl,
|
|
@@ -67,5 +75,72 @@ export class WalletNamespace {
|
|
|
67
75
|
}
|
|
68
76
|
return { checkoutUrl: ok.checkout_url, sessionId: ok.session_id };
|
|
69
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read the user's current wallet balance.
|
|
80
|
+
*
|
|
81
|
+
* Routed through the compute proxy at `${compute}/v1/invoke` because
|
|
82
|
+
* that's where the Aithos-platform DDB IAM lives — keeps the wallet
|
|
83
|
+
* Lambda focused on Stripe and avoids granting read-on-wallet to a
|
|
84
|
+
* second function. The call is gated by the same signed-envelope
|
|
85
|
+
* verification as `invokeBedrock`: only the owner of the user's
|
|
86
|
+
* `#public` key can read their balance.
|
|
87
|
+
*
|
|
88
|
+
* Returns `{ balance: 0, exists: false, dailySpent: 0 }` for an
|
|
89
|
+
* identity that has never been funded — the call still succeeds,
|
|
90
|
+
* the row just doesn't exist yet.
|
|
91
|
+
*
|
|
92
|
+
* @throws {AithosSDKError} on protocol errors (network, http,
|
|
93
|
+
* envelope-verify failures from the proxy).
|
|
94
|
+
*/
|
|
95
|
+
async getBalance(args = {}) {
|
|
96
|
+
const { auth, appDid, endpoints, fetch: fetchImpl } = this.#deps;
|
|
97
|
+
const owner = auth._getOwnerSigners();
|
|
98
|
+
if (!owner || owner.destroyed) {
|
|
99
|
+
throw new AithosSDKError("sdk_no_owner", "no owner signed in; sign in first");
|
|
100
|
+
}
|
|
101
|
+
const publicKp = ownerKeyPair(owner, "public");
|
|
102
|
+
const url = computeInvokeUrl(endpoints);
|
|
103
|
+
const params = { app_did: appDid };
|
|
104
|
+
const envelope = buildSignedEnvelope({
|
|
105
|
+
iss: owner.did,
|
|
106
|
+
aud: url,
|
|
107
|
+
method: "aithos.wallet_get_balance",
|
|
108
|
+
verificationMethod: `${owner.did}#public`,
|
|
109
|
+
params,
|
|
110
|
+
signer: publicKp,
|
|
111
|
+
});
|
|
112
|
+
let res;
|
|
113
|
+
try {
|
|
114
|
+
res = await fetchImpl(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "content-type": "application/json" },
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
jsonrpc: "2.0",
|
|
119
|
+
id: "aithos.wallet_get_balance",
|
|
120
|
+
method: "aithos.wallet_get_balance",
|
|
121
|
+
params: { ...params, _envelope: envelope },
|
|
122
|
+
}),
|
|
123
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
throw new AithosSDKError("network", e.message);
|
|
128
|
+
}
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
throw new AithosSDKError("http", `HTTP ${res.status} ${res.statusText}`, { status: res.status });
|
|
131
|
+
}
|
|
132
|
+
const body = (await res.json());
|
|
133
|
+
if (body.error) {
|
|
134
|
+
throw new AithosSDKError(String(body.error.code), body.error.message, body.error.data ? { data: body.error.data } : undefined);
|
|
135
|
+
}
|
|
136
|
+
if (!body.result) {
|
|
137
|
+
throw new AithosSDKError("empty", "empty result from wallet_get_balance");
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
balance: typeof body.result.balance === "number" ? body.result.balance : 0,
|
|
141
|
+
dailySpent: typeof body.result.dailySpent === "number" ? body.result.dailySpent : 0,
|
|
142
|
+
exists: body.result.exists === true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
70
145
|
}
|
|
71
146
|
//# sourceMappingURL=wallet.js.map
|