@aithos/sdk 0.1.0-alpha.1 → 0.1.0-alpha.10

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.
Files changed (55) hide show
  1. package/README.md +45 -0
  2. package/dist/src/auth-api.d.ts +41 -0
  3. package/dist/src/auth-api.js +82 -0
  4. package/dist/src/auth.d.ts +166 -0
  5. package/dist/src/auth.js +730 -0
  6. package/dist/src/compute.d.ts +27 -6
  7. package/dist/src/compute.js +67 -10
  8. package/dist/src/ethos.d.ts +117 -1
  9. package/dist/src/ethos.js +646 -16
  10. package/dist/src/index.d.ts +11 -4
  11. package/dist/src/index.js +31 -5
  12. package/dist/src/internal/delegate-bundle.d.ts +18 -0
  13. package/dist/src/internal/delegate-bundle.js +94 -0
  14. package/dist/src/internal/delegate-state.d.ts +45 -0
  15. package/dist/src/internal/delegate-state.js +120 -0
  16. package/dist/src/internal/owner-signers.d.ts +78 -0
  17. package/dist/src/internal/owner-signers.js +179 -0
  18. package/dist/src/internal/protocol-client-bridge.d.ts +8 -0
  19. package/dist/src/internal/protocol-client-bridge.js +20 -0
  20. package/dist/src/internal/recovery-file.d.ts +29 -0
  21. package/dist/src/internal/recovery-file.js +98 -0
  22. package/dist/src/internal/signer.d.ts +59 -0
  23. package/dist/src/internal/signer.js +86 -0
  24. package/dist/src/key-store.d.ts +128 -0
  25. package/dist/src/key-store.js +244 -0
  26. package/dist/src/mandates.d.ts +163 -1
  27. package/dist/src/mandates.js +286 -8
  28. package/dist/src/sdk.d.ts +36 -3
  29. package/dist/src/sdk.js +28 -22
  30. package/dist/src/session-store.d.ts +58 -0
  31. package/dist/src/session-store.js +158 -0
  32. package/dist/src/wallet.d.ts +42 -2
  33. package/dist/src/wallet.js +89 -14
  34. package/dist/test/auth-j3.test.d.ts +2 -0
  35. package/dist/test/auth-j3.test.js +391 -0
  36. package/dist/test/auth.test.d.ts +2 -0
  37. package/dist/test/auth.test.js +175 -0
  38. package/dist/test/compute-delegate-path.test.d.ts +2 -0
  39. package/dist/test/compute-delegate-path.test.js +183 -0
  40. package/dist/test/compute.test.js +22 -11
  41. package/dist/test/ethos-first-edition.test.d.ts +2 -0
  42. package/dist/test/ethos-first-edition.test.js +248 -0
  43. package/dist/test/ethos.test.d.ts +2 -0
  44. package/dist/test/ethos.test.js +219 -0
  45. package/dist/test/key-store.test.d.ts +2 -0
  46. package/dist/test/key-store.test.js +161 -0
  47. package/dist/test/mandates-compute.test.d.ts +2 -0
  48. package/dist/test/mandates-compute.test.js +256 -0
  49. package/dist/test/mandates.test.d.ts +2 -0
  50. package/dist/test/mandates.test.js +93 -0
  51. package/dist/test/sdk.test.js +70 -30
  52. package/dist/test/signer.test.d.ts +2 -0
  53. package/dist/test/signer.test.js +117 -0
  54. package/dist/test/wallet.test.js +20 -9
  55. 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
@@ -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
- readonly userDid: string;
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 `userDid`'s wallet once the
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
@@ -1,17 +1,21 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright 2026 Mathieu Colla
3
- // Wallet namespace — Stripe Checkout for credit-pack top-ups.
3
+ // Wallet namespace — credit-pack top-ups (Stripe) and balance lookup.
4
4
  //
5
- // Posts to `${wallet}/v1/wallet/topup/checkout`. The Lambda creates a
6
- // Stripe Checkout session bound to the user's DID via `metadata`, returns
7
- // the hosted URL. The frontend then redirects the user. Stripe webhook
8
- // (server-side) credits the wallet on `checkout.session.completed`.
9
- //
10
- // v0 scope: no envelope verify on the checkout endpoint the link is
11
- // single-use and metadata-bound to `user_did`. Anyone creating a session
12
- // for someone else's DID would just gift them credits, no value to an
13
- // attacker. Envelope-verify gets added in a follow-up if abuse appears.
14
- import { walletTopupCheckoutUrl, } from "./endpoints.js";
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-boundanyone
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 `userDid`'s wallet once the
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 { userDid, endpoints, fetch: fetchImpl } = this.#deps;
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: userDid,
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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auth-j3.test.d.ts.map