@aithos/sdk 0.1.0-alpha.28 → 0.1.0-alpha.30

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 CHANGED
@@ -100,6 +100,84 @@ 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
+ ## Custodial auth — onboarding users without a recovery file
104
+
105
+ Three new methods on `AithosAuth` let an app create and authenticate
106
+ its end-users via a server-managed custody flow — the user only needs
107
+ an email address and a password sent by mail. No recovery file, no
108
+ Google account, no client-side cryptography to handle.
109
+
110
+ The model is honest custody: Aithos KMS-wraps the user's Ed25519
111
+ identity seeds, and unwraps them on every sign-in after password
112
+ verification. Equivalent to how Coinbase or any hosted SaaS keeps your
113
+ private key. Annunciated to the user in the welcome email.
114
+
115
+ ```ts
116
+ import { AithosSDK } from "@aithos/sdk";
117
+
118
+ // ─── Server-side: sign-up ───────────────────────────────────────────
119
+ // MUST run on your backend. The API key is a server secret —
120
+ // provisioned by Aithos via the operator runbook.
121
+ const sdk = new AithosSDK({ identity });
122
+ const result = await sdk.auth.signUpCustodial({
123
+ apiKey: process.env.AITHOS_API_KEY!,
124
+ email: "alice@example.com",
125
+ displayName: "Alice",
126
+ });
127
+ // → { userId, did, handle, email, mailSent }
128
+ // The user receives an email with their password and a sign-in link.
129
+
130
+ // ─── Browser-side: sign-in ──────────────────────────────────────────
131
+ // User pastes the password from their mail into your sign-in form,
132
+ // then your frontend calls this. No API key needed — the password
133
+ // is the credential.
134
+ const { session, passwordMustChange } = await sdk.auth.signInCustodial({
135
+ email: "alice@example.com",
136
+ password: "MyTempPass32chars",
137
+ });
138
+ // Local KeyStore is now hydrated with the 4 Ed25519 sphere seeds —
139
+ // the user can publish ethos editions, mint mandates, invoke compute,
140
+ // exactly as if they had signed in via a recovery file or Google SSO.
141
+ if (passwordMustChange) {
142
+ // Optional: nudge the user to set their own password via the
143
+ // standard reset flow.
144
+ }
145
+
146
+ // ─── Browser-side: request password reset ───────────────────────────
147
+ // The backend always returns silently (anti-enumeration). If the email
148
+ // is registered AND in custodial mode AND not in cooldown AND under the
149
+ // daily cap, a magic-link email is sent to the address.
150
+ await sdk.auth.requestPasswordReset({ email: "alice@example.com" });
151
+ ```
152
+
153
+ The reset finalization (collecting the new password from the user) is
154
+ done on a small web page hosted by Aithos at `https://app.aithos.be/reset`
155
+ (or your app's own `reset_base_url` if you've registered one — see the
156
+ operator runbook). The page POSTs to `/auth/custodial/reset/finalize`
157
+ and returns the user to your sign-in page on success.
158
+
159
+ ### Getting an API key
160
+
161
+ API keys are provisioned out-of-band by Aithos. Contact the maintainer
162
+ (or use the self-service console at `aithos.be/console` when it ships
163
+ in V2). The pattern is `aithos_<env>_<32 chars b58>`. Keep it in your
164
+ backend's secrets manager — never in browser code.
165
+
166
+ ### Trade-offs vs. the zk and Google SSO flows
167
+
168
+ | | zk (recovery file) | Google SSO (KMS) | **Custodial** |
169
+ |----------------|----------------------------|----------------------|---------------|
170
+ | User burden | downloads `recovery.json` | Google consent | email only |
171
+ | Password reset | requires recovery file | re-auth via Google | magic-link mail |
172
+ | Trust model | zero-knowledge (you only) | Aithos + Google | Aithos only |
173
+ | Multi-device | re-import recovery | re-Google | email + password |
174
+ | SDK signing capability | full | full | full |
175
+
176
+ Custodial is the right default for SDK-integrated apps that want
177
+ SaaS-grade UX. zk is the right default for power users who want
178
+ sovereign custody. SSO is the right default for users already invested
179
+ in the Google ecosystem.
180
+
103
181
  ## Extracting webpages without an LLM
104
182
 
105
183
  `sdk.web` is a token-priced primitive that lets your agent read a
@@ -46,5 +46,60 @@ export interface LoginVerifyResponse {
46
46
  readonly blobVersion: number;
47
47
  }
48
48
  export declare function loginVerify(http: HttpClient, email: string, authKey: Uint8Array): Promise<LoginVerifyResponse>;
49
+ export interface CustodialSignUpApiInput {
50
+ readonly apiKey: string;
51
+ readonly email: string;
52
+ readonly displayName?: string;
53
+ readonly handleHint?: string;
54
+ }
55
+ export interface CustodialSignUpApiResponse {
56
+ readonly userId: string;
57
+ readonly did: string;
58
+ readonly handle: string;
59
+ readonly email: string;
60
+ readonly mailSent: boolean;
61
+ readonly mailMessageId?: string;
62
+ }
63
+ /**
64
+ * Provision a custodial-mode account on behalf of a registered app.
65
+ * Server-only — the API key MUST be kept off the browser (it grants the
66
+ * ability to create accounts under your app's name). Typical use site is
67
+ * the app's backend in response to a sign-up form submission.
68
+ */
69
+ export declare function custodialSignUp(http: HttpClient, input: CustodialSignUpApiInput): Promise<CustodialSignUpApiResponse>;
70
+ export interface CustodialSignInApiInput {
71
+ readonly email: string;
72
+ readonly password: string;
73
+ }
74
+ export interface CustodialSignInApiResponse {
75
+ readonly session: string;
76
+ readonly exp: number;
77
+ readonly did: string;
78
+ readonly handle: string;
79
+ readonly displayName: string;
80
+ /** Raw 32-byte Ed25519 seed — caller MUST hydrate its keystore and
81
+ * zeroize this buffer. */
82
+ readonly seed: Uint8Array;
83
+ /** Raw 32-byte vault encryption key — same lifecycle. */
84
+ readonly encKey: Uint8Array;
85
+ readonly blob: Uint8Array;
86
+ readonly blobNonce: Uint8Array;
87
+ readonly blobVersion: number;
88
+ readonly passwordMustChange: boolean;
89
+ }
90
+ export declare function custodialSignIn(http: HttpClient, input: CustodialSignInApiInput): Promise<CustodialSignInApiResponse>;
91
+ export declare function custodialResetRequest(http: HttpClient, email: string): Promise<void>;
92
+ export interface CustodialResetFinalizeApiInput {
93
+ readonly email: string;
94
+ readonly token: string;
95
+ readonly newPassword: string;
96
+ }
97
+ export interface CustodialResetFinalizeApiResponse {
98
+ readonly session: string;
99
+ readonly exp: number;
100
+ readonly did: string;
101
+ readonly handle: string;
102
+ }
103
+ export declare function custodialResetFinalize(http: HttpClient, input: CustodialResetFinalizeApiInput): Promise<CustodialResetFinalizeApiResponse>;
49
104
  export {};
50
105
  //# sourceMappingURL=auth-api.d.ts.map
@@ -99,4 +99,80 @@ export async function loginVerify(http, email, authKey) {
99
99
  blobVersion: wire.blob_version,
100
100
  };
101
101
  }
102
+ /**
103
+ * Provision a custodial-mode account on behalf of a registered app.
104
+ * Server-only — the API key MUST be kept off the browser (it grants the
105
+ * ability to create accounts under your app's name). Typical use site is
106
+ * the app's backend in response to a sign-up form submission.
107
+ */
108
+ export async function custodialSignUp(http, input) {
109
+ const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/sign-up`, {
110
+ method: "POST",
111
+ headers: {
112
+ "content-type": "application/json",
113
+ authorization: `Bearer ${input.apiKey}`,
114
+ },
115
+ body: JSON.stringify({
116
+ email: input.email,
117
+ ...(input.displayName ? { display_name: input.displayName } : {}),
118
+ ...(input.handleHint ? { handle_hint: input.handleHint } : {}),
119
+ }),
120
+ });
121
+ if (!res.ok)
122
+ throw await readError(res, "custodial_signup_failed");
123
+ const wire = (await res.json());
124
+ return {
125
+ userId: wire.user_id,
126
+ did: wire.did,
127
+ handle: wire.handle,
128
+ email: wire.email,
129
+ mailSent: wire.mail_sent,
130
+ ...(wire.mail_message_id !== undefined
131
+ ? { mailMessageId: wire.mail_message_id }
132
+ : {}),
133
+ };
134
+ }
135
+ export async function custodialSignIn(http, input) {
136
+ const wire = await postJson(http, "/auth/custodial/sign-in", { email: input.email, password: input.password });
137
+ return {
138
+ session: wire.session,
139
+ exp: wire.exp,
140
+ did: wire.did,
141
+ handle: wire.handle,
142
+ displayName: wire.display_name,
143
+ seed: b64ToBytes(wire.seed_b64),
144
+ encKey: b64ToBytes(wire.enc_key_b64),
145
+ blob: wire.blob_b64 ? b64ToBytes(wire.blob_b64) : new Uint8Array(0),
146
+ blobNonce: wire.blob_nonce_b64
147
+ ? b64ToBytes(wire.blob_nonce_b64)
148
+ : new Uint8Array(0),
149
+ blobVersion: wire.blob_version,
150
+ passwordMustChange: wire.password_must_change,
151
+ };
152
+ }
153
+ /* ---- POST /auth/custodial/reset/request --------------------------------- */
154
+ export async function custodialResetRequest(http, email) {
155
+ // Backend always returns 200 { ok: true } regardless. We accept any
156
+ // 2xx body, even non-JSON, to be defensive.
157
+ const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/reset/request`, {
158
+ method: "POST",
159
+ headers: { "content-type": "application/json" },
160
+ body: JSON.stringify({ email }),
161
+ });
162
+ if (!res.ok)
163
+ throw await readError(res, "custodial_reset_request_failed");
164
+ }
165
+ export async function custodialResetFinalize(http, input) {
166
+ const wire = await postJson(http, "/auth/custodial/reset/finalize", {
167
+ email: input.email,
168
+ token: input.token,
169
+ new_password: input.newPassword,
170
+ });
171
+ return {
172
+ session: wire.session,
173
+ exp: wire.exp,
174
+ did: wire.did,
175
+ handle: wire.handle,
176
+ };
177
+ }
102
178
  //# sourceMappingURL=auth-api.js.map
@@ -135,6 +135,85 @@ export interface ImportMandateInput {
135
135
  /** Delegate bundle as a Blob or already-decoded JSON string. */
136
136
  readonly bundle: Blob | string;
137
137
  }
138
+ /**
139
+ * Input to {@link AithosAuth.signUpCustodial}. Server-side only — the
140
+ * API key MUST stay off the browser (it grants account-creation
141
+ * authority under your app's name). Typical use site: your app's
142
+ * backend in response to a sign-up form submission.
143
+ */
144
+ export interface CustodialSignUpInput {
145
+ /** Bearer API key issued to your app (`aithos_<env>_<32b58>`). */
146
+ readonly apiKey: string;
147
+ /** Email address of the new user. Will receive the welcome mail. */
148
+ readonly email: string;
149
+ /** Optional display name. Capped at 200 chars by the backend. */
150
+ readonly displayName?: string;
151
+ /** Optional handle hint. Backend may sanitise or replace. */
152
+ readonly handleHint?: string;
153
+ }
154
+ /**
155
+ * Result of {@link AithosAuth.signUpCustodial}. The raw password is
156
+ * NEVER returned in this response — it lives only in the welcome email
157
+ * sent to the user via SES. `mailSent: false` means the account row
158
+ * was created but the email handoff to SES failed; the operator can
159
+ * trigger a manual resend.
160
+ */
161
+ export interface CustodialSignUpResult {
162
+ readonly userId: string;
163
+ readonly did: string;
164
+ readonly handle: string;
165
+ readonly email: string;
166
+ readonly mailSent: boolean;
167
+ readonly mailMessageId?: string;
168
+ }
169
+ export interface CustodialSignInInput {
170
+ readonly email: string;
171
+ readonly password: string;
172
+ }
173
+ /**
174
+ * Active custodial session. Same JWT-backed shape as {@link AithosSession}
175
+ * but adds a `passwordMustChange` flag the UI can honour to nudge the
176
+ * user toward a `requestPasswordReset` on first login.
177
+ */
178
+ export interface CustodialSignInResult {
179
+ readonly session: AithosSession;
180
+ readonly passwordMustChange: boolean;
181
+ }
182
+ export interface RequestPasswordResetInput {
183
+ readonly email: string;
184
+ }
185
+ /**
186
+ * Input to {@link AithosAuth.applyPasswordReset}. Finalises a password
187
+ * reset started by {@link AithosAuth.requestPasswordReset}. The `email`
188
+ * and `token` come straight from the magic-link URL that landed in the
189
+ * user's inbox (`?email=…&token=…`); the `newPassword` is what the user
190
+ * just typed in the reset page.
191
+ */
192
+ export interface ApplyPasswordResetInput {
193
+ /** Email address whose password is being reset. */
194
+ readonly email: string;
195
+ /** Raw reset token extracted from the magic-link URL query string. */
196
+ readonly token: string;
197
+ /** New password — must satisfy the backend's policy (≥ 10 chars). */
198
+ readonly newPassword: string;
199
+ }
200
+ /**
201
+ * Result of {@link AithosAuth.applyPasswordReset}. Carries a fresh JWT
202
+ * session so the UI can either redirect to a "you're now signed in"
203
+ * landing or prompt the user to sign in explicitly with their new
204
+ * credentials — same {@link CustodialSignInResult} shape as a normal
205
+ * sign-in.
206
+ *
207
+ * Note: unlike {@link signInCustodial}, this DOES NOT hydrate the local
208
+ * keystore. The reset path on the auth Lambda re-wraps the seed bundle
209
+ * with KMS but doesn't return it (the user just typed a password — they
210
+ * still need to sign in once to materialise the seeds locally). The
211
+ * {@link AithosSession} returned here lets the app store the JWT and
212
+ * call {@link signInCustodial} to complete hydration.
213
+ */
214
+ export interface ApplyPasswordResetResult {
215
+ readonly session: AithosSession;
216
+ }
138
217
  export declare class AithosAuth {
139
218
  #private;
140
219
  readonly authBaseUrl: string;
@@ -248,6 +327,81 @@ export declare class AithosAuth {
248
327
  * already completed setup; nothing to do).
249
328
  */
250
329
  completeSsoFirstLogin(input: CompleteSsoFirstLoginInput): Promise<CompleteSsoFirstLoginResult>;
330
+ /**
331
+ * Provision a custodial-mode account on behalf of a registered app.
332
+ *
333
+ * SERVER-ONLY — the API key MUST stay off the browser. The raw user
334
+ * password is generated server-side and sent to the user via the
335
+ * Aithos welcome email; it is NEVER returned in this response.
336
+ *
337
+ * Typical use site: your app's backend in response to a sign-up form
338
+ * submission. The frontend never sees the API key, only the resulting
339
+ * `{ userId, did, handle, email, mailSent }` it can show to the user
340
+ * ("we just sent you a mail with your credentials").
341
+ *
342
+ * Errors map to `AithosSDKError` codes:
343
+ * - `auth_missing_api_key` (your code passed empty apiKey)
344
+ * - `auth_invalid_api_key` (Bearer rejected by backend)
345
+ * - `auth_api_key_revoked` (backend marked the key revoked)
346
+ * - `auth_email_exists` (409 — email already registered)
347
+ * - `auth_email_invalid` (400 — bad email format)
348
+ * - `auth_mail_send_failed` (502 — DDB row exists but SES failed)
349
+ * - `auth_custodial_signup_failed` (catch-all)
350
+ */
351
+ signUpCustodial(input: CustodialSignUpInput): Promise<CustodialSignUpResult>;
352
+ /**
353
+ * Authenticate a custodial-mode user with email + password. Single
354
+ * round-trip: returns a fresh JWT session AND hydrates the local
355
+ * KeyStore with the user's 4 Ed25519 seeds (KMS-unwrapped server-side
356
+ * after Argon2id verify).
357
+ *
358
+ * After this returns, the SDK is ready to publish ethos editions,
359
+ * invoke compute, mint mandates, etc. — exactly as if the user had
360
+ * signed in via {@link signIn} (zk) or {@link handleCallback} (SSO).
361
+ *
362
+ * Errors map to `AithosSDKError` codes:
363
+ * - `auth_invalid_input` (your code passed empty fields)
364
+ * - `auth_invalid_credentials` (401 — wrong email / wrong password)
365
+ * - `auth_wrong_auth_mode` (403 — user exists in another flow)
366
+ */
367
+ signInCustodial(input: CustodialSignInInput): Promise<CustodialSignInResult>;
368
+ /**
369
+ * Trigger a password-reset email to the given address. Backend ALWAYS
370
+ * resolves silently (no enumeration) — caller cannot tell whether the
371
+ * email is registered or not. The mail itself, if sent, contains a
372
+ * magic-link URL of shape `<resetBaseUrl>?token=<raw>&email=<email>`.
373
+ *
374
+ * Per-email rate limits apply server-side (5 mails/day, 5 min cooldown
375
+ * between consecutive requests). Calls during cooldown silently no-op
376
+ * the mail send while still returning success here.
377
+ */
378
+ requestPasswordReset(input: RequestPasswordResetInput): Promise<void>;
379
+ /**
380
+ * Finalise a password reset using the magic-link token sent to the
381
+ * user's inbox by {@link requestPasswordReset}.
382
+ *
383
+ * Typical use site: the page mounted on the reset URL declared in
384
+ * `aithos-auth-apps.reset_base_url`. The page reads `email` and
385
+ * `token` from `window.location.search`, prompts the user for a new
386
+ * password, then calls this method.
387
+ *
388
+ * On success, the returned {@link AithosSession} is persisted to the
389
+ * session store but the local keystore is NOT hydrated — the backend
390
+ * does not return the seed bundle on this endpoint. To get a fully
391
+ * usable session (one that can sign envelopes), follow up with
392
+ * {@link signInCustodial} using the email + new password. The two
393
+ * round-trips can be hidden inside a single UI action: reset → auto
394
+ * sign-in → redirect to dashboard.
395
+ *
396
+ * Errors map to `AithosSDKError` codes:
397
+ * - `auth_invalid_input` (your code passed empty fields)
398
+ * - `auth_reset_token_invalid` (400 — token forged / wrong email)
399
+ * - `auth_reset_token_expired` (410 — token TTL elapsed)
400
+ * - `auth_reset_token_consumed` (409 — already used)
401
+ * - `auth_password_too_short` (400 — < 10 chars)
402
+ * - `auth_custodial_reset_failed` (catch-all)
403
+ */
404
+ applyPasswordReset(input: ApplyPasswordResetInput): Promise<ApplyPasswordResetResult>;
251
405
  signOut(): Promise<void>;
252
406
  }
253
407
  //# sourceMappingURL=auth.d.ts.map
package/dist/src/auth.js CHANGED
@@ -21,7 +21,7 @@
21
21
  // keyStore is the source of truth for "is the user signed in", the
22
22
  // JWT is auxiliary for compute/wallet.
23
23
  import { buildBlobPlaintext, buildSignedEnvelope, createBrowserIdentity, decryptBlob, DEFAULT_KDF, deriveAuthAndEncKeys, encryptBlob, parseBlob, randomNonce, randomSalt, serializeBlob, signedDidDocument, zeroize, } from "@aithos/protocol-client";
24
- import { loginChallenge, loginVerify, putBlob, registerAccount, } from "./auth-api.js";
24
+ import { custodialResetFinalize, custodialResetRequest, custodialSignIn, custodialSignUp, loginChallenge, loginVerify, putBlob, registerAccount, } from "./auth-api.js";
25
25
  import { defaultSessionStore, } from "./session-store.js";
26
26
  import { defaultKeyStore, } from "./key-store.js";
27
27
  import { parseDelegateBundle, readDelegateBundleText, } from "./internal/delegate-bundle.js";
@@ -771,6 +771,188 @@ export class AithosAuth {
771
771
  };
772
772
  }
773
773
  /* ------------------------------------------------------------------------ */
774
+ /* Custodial flow (V2 — see PLATFORM-AUTH-PASSWORD-V2-PLAN.md) */
775
+ /* ------------------------------------------------------------------------ */
776
+ /**
777
+ * Provision a custodial-mode account on behalf of a registered app.
778
+ *
779
+ * SERVER-ONLY — the API key MUST stay off the browser. The raw user
780
+ * password is generated server-side and sent to the user via the
781
+ * Aithos welcome email; it is NEVER returned in this response.
782
+ *
783
+ * Typical use site: your app's backend in response to a sign-up form
784
+ * submission. The frontend never sees the API key, only the resulting
785
+ * `{ userId, did, handle, email, mailSent }` it can show to the user
786
+ * ("we just sent you a mail with your credentials").
787
+ *
788
+ * Errors map to `AithosSDKError` codes:
789
+ * - `auth_missing_api_key` (your code passed empty apiKey)
790
+ * - `auth_invalid_api_key` (Bearer rejected by backend)
791
+ * - `auth_api_key_revoked` (backend marked the key revoked)
792
+ * - `auth_email_exists` (409 — email already registered)
793
+ * - `auth_email_invalid` (400 — bad email format)
794
+ * - `auth_mail_send_failed` (502 — DDB row exists but SES failed)
795
+ * - `auth_custodial_signup_failed` (catch-all)
796
+ */
797
+ async signUpCustodial(input) {
798
+ if (!input.apiKey) {
799
+ throw new AithosSDKError("auth_missing_api_key", "signUpCustodial: apiKey is required");
800
+ }
801
+ if (!input.email) {
802
+ throw new AithosSDKError("auth_invalid_input", "signUpCustodial: email is required");
803
+ }
804
+ return custodialSignUp({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
805
+ }
806
+ /**
807
+ * Authenticate a custodial-mode user with email + password. Single
808
+ * round-trip: returns a fresh JWT session AND hydrates the local
809
+ * KeyStore with the user's 4 Ed25519 seeds (KMS-unwrapped server-side
810
+ * after Argon2id verify).
811
+ *
812
+ * After this returns, the SDK is ready to publish ethos editions,
813
+ * invoke compute, mint mandates, etc. — exactly as if the user had
814
+ * signed in via {@link signIn} (zk) or {@link handleCallback} (SSO).
815
+ *
816
+ * Errors map to `AithosSDKError` codes:
817
+ * - `auth_invalid_input` (your code passed empty fields)
818
+ * - `auth_invalid_credentials` (401 — wrong email / wrong password)
819
+ * - `auth_wrong_auth_mode` (403 — user exists in another flow)
820
+ */
821
+ async signInCustodial(input) {
822
+ if (!input.email || !input.password) {
823
+ throw new AithosSDKError("auth_invalid_input", "signInCustodial: email and password are required");
824
+ }
825
+ const resp = await custodialSignIn({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
826
+ // Split the 128-byte seed bundle into the four sphere seeds. The
827
+ // backend lays them out in the canonical order
828
+ // [root || public || circle || self] (cf. seed-wrapper.ts).
829
+ if (resp.seed.byteLength !== 128) {
830
+ // Legacy 32-byte rows shouldn't happen in production (we wiped the
831
+ // single test row before redeploying with the 4-seed bundle), but
832
+ // we surface a clear error rather than silently corrupting the
833
+ // identity.
834
+ zeroize(resp.seed);
835
+ zeroize(resp.encKey);
836
+ throw new AithosSDKError("auth_custodial_seed_format", `signInCustodial: expected 128-byte seed bundle, got ${resp.seed.byteLength}`);
837
+ }
838
+ const seedRoot = resp.seed.slice(0, 32);
839
+ const seedPublic = resp.seed.slice(32, 64);
840
+ const seedCircle = resp.seed.slice(64, 96);
841
+ const seedSelf = resp.seed.slice(96, 128);
842
+ // Stored shape uses hex strings; round-trip through bytesToHex
843
+ // so the keyStore record is identical to what signUp(zk) writes.
844
+ const stored = {
845
+ version: "0.1.0-hex",
846
+ did: resp.did,
847
+ handle: resp.handle,
848
+ displayName: resp.displayName,
849
+ seedsHex: {
850
+ root: bytesToHex(seedRoot),
851
+ public: bytesToHex(seedPublic),
852
+ circle: bytesToHex(seedCircle),
853
+ self: bytesToHex(seedSelf),
854
+ },
855
+ savedAt: new Date().toISOString(),
856
+ };
857
+ // Zeroize the raw bundle + the split copies now that they've been
858
+ // serialised into the keyStore record (hex strings live in the
859
+ // record; the original bytes can go).
860
+ zeroize(resp.seed);
861
+ zeroize(seedRoot);
862
+ zeroize(seedPublic);
863
+ zeroize(seedCircle);
864
+ zeroize(seedSelf);
865
+ // The enc_key is informational here — the custodial blob is empty
866
+ // at first login. We still don't keep it in memory.
867
+ zeroize(resp.encKey);
868
+ // Hydrate in-memory owner signers from the freshly-stored material.
869
+ if (this.#ownerSigners)
870
+ this.#ownerSigners.destroy();
871
+ this.#ownerSigners = OwnerSigners.fromStoredOwnerKeys(stored);
872
+ await this.#keyStore.saveOwner(stored);
873
+ const session = {
874
+ session: resp.session,
875
+ exp: resp.exp,
876
+ did: resp.did,
877
+ handle: resp.handle,
878
+ blob_b64: bytesToB64Public(resp.blob),
879
+ blob_nonce_b64: bytesToB64Public(resp.blobNonce),
880
+ blob_version: resp.blobVersion,
881
+ enc_key_b64: "",
882
+ is_first_login: resp.passwordMustChange,
883
+ };
884
+ this.#sessionStore.set(session);
885
+ return { session, passwordMustChange: resp.passwordMustChange };
886
+ }
887
+ /**
888
+ * Trigger a password-reset email to the given address. Backend ALWAYS
889
+ * resolves silently (no enumeration) — caller cannot tell whether the
890
+ * email is registered or not. The mail itself, if sent, contains a
891
+ * magic-link URL of shape `<resetBaseUrl>?token=<raw>&email=<email>`.
892
+ *
893
+ * Per-email rate limits apply server-side (5 mails/day, 5 min cooldown
894
+ * between consecutive requests). Calls during cooldown silently no-op
895
+ * the mail send while still returning success here.
896
+ */
897
+ async requestPasswordReset(input) {
898
+ if (!input.email) {
899
+ throw new AithosSDKError("auth_invalid_input", "requestPasswordReset: email is required");
900
+ }
901
+ await custodialResetRequest({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input.email);
902
+ }
903
+ /**
904
+ * Finalise a password reset using the magic-link token sent to the
905
+ * user's inbox by {@link requestPasswordReset}.
906
+ *
907
+ * Typical use site: the page mounted on the reset URL declared in
908
+ * `aithos-auth-apps.reset_base_url`. The page reads `email` and
909
+ * `token` from `window.location.search`, prompts the user for a new
910
+ * password, then calls this method.
911
+ *
912
+ * On success, the returned {@link AithosSession} is persisted to the
913
+ * session store but the local keystore is NOT hydrated — the backend
914
+ * does not return the seed bundle on this endpoint. To get a fully
915
+ * usable session (one that can sign envelopes), follow up with
916
+ * {@link signInCustodial} using the email + new password. The two
917
+ * round-trips can be hidden inside a single UI action: reset → auto
918
+ * sign-in → redirect to dashboard.
919
+ *
920
+ * Errors map to `AithosSDKError` codes:
921
+ * - `auth_invalid_input` (your code passed empty fields)
922
+ * - `auth_reset_token_invalid` (400 — token forged / wrong email)
923
+ * - `auth_reset_token_expired` (410 — token TTL elapsed)
924
+ * - `auth_reset_token_consumed` (409 — already used)
925
+ * - `auth_password_too_short` (400 — < 10 chars)
926
+ * - `auth_custodial_reset_failed` (catch-all)
927
+ */
928
+ async applyPasswordReset(input) {
929
+ if (!input.email || !input.token || !input.newPassword) {
930
+ throw new AithosSDKError("auth_invalid_input", "applyPasswordReset: email, token and newPassword are required");
931
+ }
932
+ const resp = await custodialResetFinalize({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
933
+ // The reset endpoint mints a JWT but doesn't ship the seed bundle —
934
+ // the caller still has to signInCustodial() to materialise the keys
935
+ // locally. We persist the session anyway so any code that reads
936
+ // `getCurrentSession()` between the reset and the follow-up sign-in
937
+ // sees the new JWT (e.g. an analytics hook).
938
+ const session = {
939
+ session: resp.session,
940
+ exp: resp.exp,
941
+ did: resp.did,
942
+ handle: resp.handle,
943
+ // No blob / enc_key on this path — the reset endpoint doesn't
944
+ // re-issue the vault. Leave the blob slots empty; the follow-up
945
+ // signInCustodial() will populate them.
946
+ blob_b64: "",
947
+ blob_nonce_b64: "",
948
+ blob_version: 0,
949
+ enc_key_b64: "",
950
+ is_first_login: false,
951
+ };
952
+ this.#sessionStore.set(session);
953
+ return { session };
954
+ }
955
+ /* ------------------------------------------------------------------------ */
774
956
  /* Sign-out */
775
957
  /* ------------------------------------------------------------------------ */
776
958
  async signOut() {
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0-alpha.26";
1
+ export declare const VERSION = "0.1.0-alpha.30";
2
2
  export { AithosSDK } from "./sdk.js";
3
3
  export type { AithosSDKConfig } from "./types.js";
4
4
  export { AithosSDKError } from "./types.js";
@@ -12,7 +12,7 @@ export { WalletNamespace } from "./wallet.js";
12
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
13
  export { WebNamespace, WEB_EXTRACT_SCOPE } from "./web.js";
14
14
  export { AithosAuth, DEFAULT_API_BASE_URL, DEFAULT_AUTH_BASE_URL, } from "./auth.js";
15
- export type { AithosAuthConfig, AithosSession, CompleteSsoFirstLoginInput, CompleteSsoFirstLoginResult, DelegateInfo, ImportMandateInput, OwnerInfo, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, } from "./auth.js";
15
+ export type { AithosAuthConfig, AithosSession, ApplyPasswordResetInput, ApplyPasswordResetResult, CompleteSsoFirstLoginInput, CompleteSsoFirstLoginResult, CustodialSignInInput, CustodialSignInResult, CustodialSignUpInput, CustodialSignUpResult, DelegateInfo, ImportMandateInput, OwnerInfo, RequestPasswordResetInput, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, } from "./auth.js";
16
16
  export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, type AithosSessionStore, } from "./session-store.js";
17
17
  export { DEFAULT_KEYSTORE_DB_NAME, defaultKeyStore, indexedDbKeyStore, memoryKeyStore, type AithosKeyStore, type StoredDelegateKeys, type StoredOwnerKeys, } from "./key-store.js";
18
18
  export { EthosClient, EthosNamespace, EthosZone, ZONE_NAMES, } from "./ethos.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.26";
20
+ export const VERSION = "0.1.0-alpha.30";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.28",
3
+ "version": "0.1.0-alpha.30",
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",