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

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.
@@ -46,27 +46,101 @@ 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
+ /**
50
+ * Input for {@link custodialSignUp}. Caller authenticates the app via ONE
51
+ * of `apiKey` (server-only secret) or `publicKey` (browser-safe). The
52
+ * user's `password` is always required — sign-up no longer auto-generates
53
+ * one server-side. The created account starts in a *pending* state and
54
+ * the user must click the link sent to their inbox before they can
55
+ * sign in.
56
+ */
49
57
  export interface CustodialSignUpApiInput {
50
- readonly apiKey: string;
58
+ /** Server-only Bearer secret: `aithos_<env>_<…>`. Mutually exclusive
59
+ * with `publicKey`. Use this from your backend. */
60
+ readonly apiKey?: string;
61
+ /** Browser-safe public client key: `pk_<env>_<…>`. Mutually exclusive
62
+ * with `apiKey`. The browser sends its `Origin` header alongside; the
63
+ * Aithos backend matches it against the app's allowed_origins list. */
64
+ readonly publicKey?: string;
51
65
  readonly email: string;
66
+ /** Raw password the user chose. Must be ≥ 10 chars and mix letters
67
+ * with at least one digit or symbol (server-side rule). */
68
+ readonly password: string;
52
69
  readonly displayName?: string;
53
70
  readonly handleHint?: string;
54
71
  }
55
72
  export interface CustodialSignUpApiResponse {
56
- readonly userId: string;
57
- readonly did: string;
58
- readonly handle: string;
73
+ /** Always "pending_verification" — sign-in is blocked until the
74
+ * user clicks the confirmation link. */
75
+ readonly status: "pending_verification";
59
76
  readonly email: string;
60
77
  readonly mailSent: boolean;
61
78
  readonly mailMessageId?: string;
62
79
  }
63
80
  /**
64
81
  * 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.
82
+ *
83
+ * Two integration paths:
84
+ * - **Backend** caller passes `apiKey` (server-only secret).
85
+ * - **Browser** caller passes `publicKey` (safe to ship in the bundle).
86
+ * The browser also sends its `Origin` header automatically and the
87
+ * auth backend validates it against the app's allowed_origins list.
88
+ *
89
+ * On success the account exists in DDB with `email_verified: false` and
90
+ * the response carries `status: "pending_verification"` — call
91
+ * {@link custodialVerifyEmail} after the user clicks the confirmation
92
+ * link before attempting sign-in.
68
93
  */
69
94
  export declare function custodialSignUp(http: HttpClient, input: CustodialSignUpApiInput): Promise<CustodialSignUpApiResponse>;
95
+ export interface CustodialVerifyEmailApiInput {
96
+ readonly email: string;
97
+ readonly token: string;
98
+ }
99
+ /**
100
+ * Result of consuming the verification link. Magic-link mode: a
101
+ * successful first-time consumption returns a full session payload
102
+ * (JWT + seeds) so the caller can sign the user in without prompting
103
+ * for the password. Replays of the same link (after first consumption)
104
+ * land on `status: "already_verified"` — the user is already verified
105
+ * and must use the regular sign-in flow from now on.
106
+ *
107
+ * The discriminator is the `status` field. Callers should pattern-match.
108
+ */
109
+ export type CustodialVerifyEmailApiResponse = {
110
+ readonly status: "signed_in";
111
+ readonly session: string;
112
+ readonly exp: number;
113
+ readonly did: string;
114
+ readonly handle: string;
115
+ readonly displayName: string;
116
+ readonly seed: Uint8Array;
117
+ readonly encKey: Uint8Array;
118
+ readonly blob: Uint8Array;
119
+ readonly blobNonce: Uint8Array;
120
+ readonly blobVersion: number;
121
+ } | {
122
+ readonly status: "already_verified";
123
+ readonly email: string;
124
+ };
125
+ /**
126
+ * Consume the verification token from the confirmation link. On a fresh
127
+ * click: returns a full session payload (magic-link auto-signin). On a
128
+ * replayed click of an already-consumed link: returns
129
+ * `{ status: "already_verified" }`.
130
+ *
131
+ * Throws `auth_token_invalid_or_expired` if the token is wrong, consumed,
132
+ * or past its TTL.
133
+ */
134
+ export declare function custodialVerifyEmail(http: HttpClient, input: CustodialVerifyEmailApiInput): Promise<CustodialVerifyEmailApiResponse>;
135
+ /** Re-send the verification mail for a pending account. The backend
136
+ * is anti-enum (always 200) and rate-limited 1/h/account, so this is
137
+ * safe to call even when the user state is unknown. Accepts the same
138
+ * credential families as {@link custodialSignUp}. */
139
+ export declare function custodialResendVerify(http: HttpClient, args: {
140
+ readonly email: string;
141
+ readonly apiKey?: string;
142
+ readonly publicKey?: string;
143
+ }): Promise<void>;
70
144
  export interface CustodialSignInApiInput {
71
145
  readonly email: string;
72
146
  readonly password: string;
@@ -101,19 +101,35 @@ export async function loginVerify(http, email, authKey) {
101
101
  }
102
102
  /**
103
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.
104
+ *
105
+ * Two integration paths:
106
+ * - **Backend** caller passes `apiKey` (server-only secret).
107
+ * - **Browser** caller passes `publicKey` (safe to ship in the bundle).
108
+ * The browser also sends its `Origin` header automatically and the
109
+ * auth backend validates it against the app's allowed_origins list.
110
+ *
111
+ * On success the account exists in DDB with `email_verified: false` and
112
+ * the response carries `status: "pending_verification"` — call
113
+ * {@link custodialVerifyEmail} after the user clicks the confirmation
114
+ * link before attempting sign-in.
107
115
  */
108
116
  export async function custodialSignUp(http, input) {
117
+ if (!input.apiKey && !input.publicKey) {
118
+ throw new AithosSDKError("auth_missing_api_key", "signUpCustodial requires either apiKey or publicKey");
119
+ }
120
+ if (input.apiKey && input.publicKey) {
121
+ throw new AithosSDKError("auth_invalid_input", "signUpCustodial: pass exactly one of apiKey or publicKey, not both");
122
+ }
123
+ const bearer = (input.apiKey ?? input.publicKey);
109
124
  const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/sign-up`, {
110
125
  method: "POST",
111
126
  headers: {
112
127
  "content-type": "application/json",
113
- authorization: `Bearer ${input.apiKey}`,
128
+ authorization: `Bearer ${bearer}`,
114
129
  },
115
130
  body: JSON.stringify({
116
131
  email: input.email,
132
+ password: input.password,
117
133
  ...(input.displayName ? { display_name: input.displayName } : {}),
118
134
  ...(input.handleHint ? { handle_hint: input.handleHint } : {}),
119
135
  }),
@@ -122,9 +138,7 @@ export async function custodialSignUp(http, input) {
122
138
  throw await readError(res, "custodial_signup_failed");
123
139
  const wire = (await res.json());
124
140
  return {
125
- userId: wire.user_id,
126
- did: wire.did,
127
- handle: wire.handle,
141
+ status: "pending_verification",
128
142
  email: wire.email,
129
143
  mailSent: wire.mail_sent,
130
144
  ...(wire.mail_message_id !== undefined
@@ -132,6 +146,64 @@ export async function custodialSignUp(http, input) {
132
146
  : {}),
133
147
  };
134
148
  }
149
+ /**
150
+ * Consume the verification token from the confirmation link. On a fresh
151
+ * click: returns a full session payload (magic-link auto-signin). On a
152
+ * replayed click of an already-consumed link: returns
153
+ * `{ status: "already_verified" }`.
154
+ *
155
+ * Throws `auth_token_invalid_or_expired` if the token is wrong, consumed,
156
+ * or past its TTL.
157
+ */
158
+ export async function custodialVerifyEmail(http, input) {
159
+ const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/verify`, {
160
+ method: "POST",
161
+ headers: { "content-type": "application/json" },
162
+ body: JSON.stringify({ email: input.email, token: input.token }),
163
+ });
164
+ if (!res.ok)
165
+ throw await readError(res, "custodial_verify_failed");
166
+ const wire = (await res.json());
167
+ if (wire.status === "already_verified") {
168
+ return { status: "already_verified", email: wire.email };
169
+ }
170
+ return {
171
+ status: "signed_in",
172
+ session: wire.session,
173
+ exp: wire.exp,
174
+ did: wire.did,
175
+ handle: wire.handle,
176
+ displayName: wire.display_name,
177
+ seed: b64ToBytes(wire.seed_b64),
178
+ encKey: b64ToBytes(wire.enc_key_b64),
179
+ blob: wire.blob_b64 ? b64ToBytes(wire.blob_b64) : new Uint8Array(0),
180
+ blobNonce: wire.blob_nonce_b64
181
+ ? b64ToBytes(wire.blob_nonce_b64)
182
+ : new Uint8Array(0),
183
+ blobVersion: wire.blob_version,
184
+ };
185
+ }
186
+ /* ---- POST /auth/custodial/verify/resend -------------------------------- */
187
+ /** Re-send the verification mail for a pending account. The backend
188
+ * is anti-enum (always 200) and rate-limited 1/h/account, so this is
189
+ * safe to call even when the user state is unknown. Accepts the same
190
+ * credential families as {@link custodialSignUp}. */
191
+ export async function custodialResendVerify(http, args) {
192
+ if (!args.apiKey && !args.publicKey) {
193
+ throw new AithosSDKError("auth_missing_api_key", "resendVerificationEmail requires either apiKey or publicKey");
194
+ }
195
+ const bearer = (args.apiKey ?? args.publicKey);
196
+ const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/verify/resend`, {
197
+ method: "POST",
198
+ headers: {
199
+ "content-type": "application/json",
200
+ authorization: `Bearer ${bearer}`,
201
+ },
202
+ body: JSON.stringify({ email: args.email }),
203
+ });
204
+ if (!res.ok)
205
+ throw await readError(res, "custodial_resend_failed");
206
+ }
135
207
  export async function custodialSignIn(http, input) {
136
208
  const wire = await postJson(http, "/auth/custodial/sign-in", { email: input.email, password: input.password });
137
209
  return {
@@ -22,6 +22,22 @@ export interface AithosAuthConfig {
22
22
  readonly sessionStore?: AithosSessionStore;
23
23
  /** Pluggable key persistence. Defaults to {@link defaultKeyStore}. */
24
24
  readonly keyStore?: AithosKeyStore;
25
+ /**
26
+ * Public client key issued by Aithos for browser callers
27
+ * (`pk_<env>_<…>`). When set, the custodial endpoints (`signUpCustodial`,
28
+ * `verifyEmail`, `resendVerificationEmail`) authenticate as this app
29
+ * by default — the caller no longer has to repeat the key on every
30
+ * call. The corresponding `allowed_origins` allowlist must include the
31
+ * current page's origin.
32
+ *
33
+ * Safe to ship in the browser bundle: it grants nothing beyond what
34
+ * a visitor of the app can already do, is gated by Origin on every
35
+ * call, and is rate-limited per IP by the backend.
36
+ *
37
+ * If your app authenticates with a SECRET Bearer API key from a
38
+ * backend instead, leave this unset and pass `apiKey` per call.
39
+ */
40
+ readonly publicKey?: string;
25
41
  }
26
42
  /**
27
43
  * Active Aithos session. Returned by JWT-backed entry points
@@ -136,36 +152,85 @@ export interface ImportMandateInput {
136
152
  readonly bundle: Blob | string;
137
153
  }
138
154
  /**
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.
155
+ * Input to {@link AithosAuth.signUpCustodial}.
156
+ *
157
+ * The caller authenticates as an app via ONE of:
158
+ * - `apiKey` : server-only secret Bearer. Pass from your backend
159
+ * only — never ship it in browser code.
160
+ * - `publicKey` : browser-safe public client key. Safe to embed in
161
+ * the bundle; the backend gates it by Origin and
162
+ * rate-limits by IP.
163
+ *
164
+ * If you set `publicKey` on the {@link AithosAuth} constructor, omit
165
+ * both fields here — the default credential is used.
166
+ *
167
+ * The `password` is always required and is chosen by the user (sign-up
168
+ * no longer auto-generates one). The created account starts as
169
+ * **pending** (`email_verified=false`); the user must click the
170
+ * confirmation link sent by SES before {@link signInCustodial} works.
143
171
  */
144
172
  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. */
173
+ /** Server-only Bearer secret. Mutually exclusive with `publicKey`. */
174
+ readonly apiKey?: string;
175
+ /** Browser-safe public client key. Mutually exclusive with `apiKey`.
176
+ * Overrides the constructor's default `publicKey` when provided. */
177
+ readonly publicKey?: string;
178
+ /** Email address of the new user. Will receive the verification mail. */
148
179
  readonly email: string;
180
+ /** Raw password the user chose. ≥ 10 chars, mix of letters with
181
+ * ≥ 1 digit or symbol. Enforced server-side. */
182
+ readonly password: string;
149
183
  /** Optional display name. Capped at 200 chars by the backend. */
150
184
  readonly displayName?: string;
151
185
  /** Optional handle hint. Backend may sanitise or replace. */
152
186
  readonly handleHint?: string;
153
187
  }
154
188
  /**
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.
189
+ * Result of {@link AithosAuth.signUpCustodial}. Always carries
190
+ * `status: "pending_verification"`the user must click the link in
191
+ * their inbox before sign-in works. If `mailSent` is false the row
192
+ * exists in DDB anyway; trigger {@link AithosAuth.resendVerificationEmail}
193
+ * to retry the SES send.
160
194
  */
161
195
  export interface CustodialSignUpResult {
162
- readonly userId: string;
163
- readonly did: string;
164
- readonly handle: string;
196
+ readonly status: "pending_verification";
165
197
  readonly email: string;
166
198
  readonly mailSent: boolean;
167
199
  readonly mailMessageId?: string;
168
200
  }
201
+ /** Input to {@link AithosAuth.verifyEmail}. Both fields come straight
202
+ * out of the `?email=&token=` query string of the confirmation URL. */
203
+ export interface VerifyEmailInput {
204
+ readonly email: string;
205
+ readonly token: string;
206
+ }
207
+ /**
208
+ * Result of {@link AithosAuth.verifyEmail}. Discriminated by `status`.
209
+ *
210
+ * - `"signed_in"` (magic-link mode): the user has been authenticated
211
+ * in this call. A JWT session is persisted to the session store and
212
+ * the local keystore is hydrated with the unwrapped seed bundle.
213
+ * The caller can navigate the user straight to a logged-in area.
214
+ * - `"already_verified"`: the verification link had already been
215
+ * consumed on a previous click. No session is minted (the token is
216
+ * spent). The caller should route the user to the sign-in form.
217
+ */
218
+ export type VerifyEmailResult = {
219
+ readonly status: "signed_in";
220
+ readonly session: AithosSession;
221
+ readonly passwordMustChange: false;
222
+ } | {
223
+ readonly status: "already_verified";
224
+ readonly email: string;
225
+ };
226
+ /** Input to {@link AithosAuth.resendVerificationEmail}. The `email` is
227
+ * required; credential overrides follow the same rules as
228
+ * {@link CustodialSignUpInput}. */
229
+ export interface ResendVerificationInput {
230
+ readonly email: string;
231
+ readonly apiKey?: string;
232
+ readonly publicKey?: string;
233
+ }
169
234
  export interface CustodialSignInInput {
170
235
  readonly email: string;
171
236
  readonly password: string;
@@ -330,25 +395,66 @@ export declare class AithosAuth {
330
395
  /**
331
396
  * Provision a custodial-mode account on behalf of a registered app.
332
397
  *
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.
398
+ * Two integration patterns:
399
+ * - **Frontend-only** apps : set `publicKey` on the constructor
400
+ * (or on this call). Safe to ship in browser bundles — the
401
+ * backend gates each request by Origin + IP rate limit.
402
+ * - **Backend-fronted** apps : the backend passes `apiKey` (secret
403
+ * Bearer); the browser never sees the credential.
336
404
  *
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").
405
+ * The created account is in a *pending* state sign-in stays blocked
406
+ * until the user clicks the confirmation link sent to their inbox.
407
+ * Call {@link verifyEmail} from the page mounted on
408
+ * `app.verify_base_url` to consume the token; afterwards
409
+ * {@link signInCustodial} works.
341
410
  *
342
411
  * Errors map to `AithosSDKError` codes:
343
- * - `auth_missing_api_key` (your code passed empty apiKey)
412
+ * - `auth_missing_api_key` (no credential provided)
344
413
  * - `auth_invalid_api_key` (Bearer rejected by backend)
345
- * - `auth_api_key_revoked` (backend marked the key revoked)
414
+ * - `auth_invalid_public_key` (public key rejected by backend)
415
+ * - `auth_api_key_revoked` / `auth_public_key_revoked`
416
+ * - `auth_origin_not_allowed` (public key + Origin not in allowlist)
417
+ * - `auth_password_too_weak` (400 — server-side strength check)
346
418
  * - `auth_email_exists` (409 — email already registered)
347
419
  * - `auth_email_invalid` (400 — bad email format)
348
420
  * - `auth_mail_send_failed` (502 — DDB row exists but SES failed)
349
421
  * - `auth_custodial_signup_failed` (catch-all)
350
422
  */
351
423
  signUpCustodial(input: CustodialSignUpInput): Promise<CustodialSignUpResult>;
424
+ /**
425
+ * Magic-link auto-signin: consume the verification token from the
426
+ * confirmation link, KMS-unwrap the seed bundle server-side, and
427
+ * hydrate the local session + keystore in one round-trip.
428
+ *
429
+ * Outcome depends on the link's state:
430
+ * - First click on a fresh link → returns
431
+ * `{ status: "signed_in", session, … }`. The session store is
432
+ * populated, the owner signers are loaded — the user is signed
433
+ * in. The caller should navigate them to a logged-in route.
434
+ * - Click of an already-consumed link → returns
435
+ * `{ status: "already_verified", email }`. No session is minted;
436
+ * the user must sign in via {@link signInCustodial}.
437
+ *
438
+ * Mount this on the page declared as `verify_base_url` in your app's
439
+ * registration. Read `email` + `token` from `window.location.search`,
440
+ * call this, branch on `result.status`.
441
+ *
442
+ * Throws `auth_token_invalid_or_expired` if the token is wrong or
443
+ * past its 1h TTL — surface a "request a fresh link" CTA in that case.
444
+ */
445
+ verifyEmail(input: VerifyEmailInput): Promise<VerifyEmailResult>;
446
+ /**
447
+ * Re-send the verification mail for a pending account. Use when the
448
+ * user reports never having received the welcome mail, or when their
449
+ * verification token expired (24h TTL).
450
+ *
451
+ * The backend is anti-enumeration (always 200) and rate-limited
452
+ * 1/h/account, so it's safe to call even when the state of `email`
453
+ * is unknown. Accepts the same credential families as
454
+ * {@link signUpCustodial}; falls back to the constructor's
455
+ * `publicKey` when neither override is set.
456
+ */
457
+ resendVerificationEmail(input: ResendVerificationInput): Promise<void>;
352
458
  /**
353
459
  * Authenticate a custodial-mode user with email + password. Single
354
460
  * round-trip: returns a fresh JWT session AND hydrates the local
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 { custodialResetFinalize, custodialResetRequest, custodialSignIn, custodialSignUp, loginChallenge, loginVerify, putBlob, registerAccount, } from "./auth-api.js";
24
+ import { custodialResendVerify, custodialResetFinalize, custodialResetRequest, custodialSignIn, custodialSignUp, custodialVerifyEmail, 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";
@@ -43,6 +43,7 @@ export class AithosAuth {
43
43
  #win;
44
44
  #sessionStore;
45
45
  #keyStore;
46
+ #publicKey;
46
47
  /** In-memory owner signers — populated after sign-in or `resume`. */
47
48
  #ownerSigners = null;
48
49
  /** Active delegate registry. */
@@ -66,6 +67,7 @@ export class AithosAuth {
66
67
  (typeof window !== "undefined" ? window : undefined);
67
68
  this.#sessionStore = config.sessionStore ?? defaultSessionStore();
68
69
  this.#keyStore = config.keyStore ?? defaultKeyStore();
70
+ this.#publicKey = config.publicKey;
69
71
  }
70
72
  /* ------------------------------------------------------------------------ */
71
73
  /* Boot-time hydration */
@@ -776,32 +778,154 @@ export class AithosAuth {
776
778
  /**
777
779
  * Provision a custodial-mode account on behalf of a registered app.
778
780
  *
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.
781
+ * Two integration patterns:
782
+ * - **Frontend-only** apps : set `publicKey` on the constructor
783
+ * (or on this call). Safe to ship in browser bundles — the
784
+ * backend gates each request by Origin + IP rate limit.
785
+ * - **Backend-fronted** apps : the backend passes `apiKey` (secret
786
+ * Bearer); the browser never sees the credential.
782
787
  *
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").
788
+ * The created account is in a *pending* state sign-in stays blocked
789
+ * until the user clicks the confirmation link sent to their inbox.
790
+ * Call {@link verifyEmail} from the page mounted on
791
+ * `app.verify_base_url` to consume the token; afterwards
792
+ * {@link signInCustodial} works.
787
793
  *
788
794
  * Errors map to `AithosSDKError` codes:
789
- * - `auth_missing_api_key` (your code passed empty apiKey)
795
+ * - `auth_missing_api_key` (no credential provided)
790
796
  * - `auth_invalid_api_key` (Bearer rejected by backend)
791
- * - `auth_api_key_revoked` (backend marked the key revoked)
797
+ * - `auth_invalid_public_key` (public key rejected by backend)
798
+ * - `auth_api_key_revoked` / `auth_public_key_revoked`
799
+ * - `auth_origin_not_allowed` (public key + Origin not in allowlist)
800
+ * - `auth_password_too_weak` (400 — server-side strength check)
792
801
  * - `auth_email_exists` (409 — email already registered)
793
802
  * - `auth_email_invalid` (400 — bad email format)
794
803
  * - `auth_mail_send_failed` (502 — DDB row exists but SES failed)
795
804
  * - `auth_custodial_signup_failed` (catch-all)
796
805
  */
797
806
  async signUpCustodial(input) {
798
- if (!input.apiKey) {
799
- throw new AithosSDKError("auth_missing_api_key", "signUpCustodial: apiKey is required");
800
- }
801
807
  if (!input.email) {
802
808
  throw new AithosSDKError("auth_invalid_input", "signUpCustodial: email is required");
803
809
  }
804
- return custodialSignUp({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
810
+ if (!input.password) {
811
+ throw new AithosSDKError("auth_invalid_input", "signUpCustodial: password is required");
812
+ }
813
+ const apiKey = input.apiKey;
814
+ const publicKey = input.publicKey ?? this.#publicKey;
815
+ if (!apiKey && !publicKey) {
816
+ throw new AithosSDKError("auth_missing_api_key", "signUpCustodial: pass apiKey, or publicKey, or set publicKey on the AithosAuth constructor");
817
+ }
818
+ return custodialSignUp({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, {
819
+ email: input.email,
820
+ password: input.password,
821
+ ...(apiKey ? { apiKey } : {}),
822
+ ...(apiKey ? {} : publicKey ? { publicKey } : {}),
823
+ ...(input.displayName ? { displayName: input.displayName } : {}),
824
+ ...(input.handleHint ? { handleHint: input.handleHint } : {}),
825
+ });
826
+ }
827
+ /**
828
+ * Magic-link auto-signin: consume the verification token from the
829
+ * confirmation link, KMS-unwrap the seed bundle server-side, and
830
+ * hydrate the local session + keystore in one round-trip.
831
+ *
832
+ * Outcome depends on the link's state:
833
+ * - First click on a fresh link → returns
834
+ * `{ status: "signed_in", session, … }`. The session store is
835
+ * populated, the owner signers are loaded — the user is signed
836
+ * in. The caller should navigate them to a logged-in route.
837
+ * - Click of an already-consumed link → returns
838
+ * `{ status: "already_verified", email }`. No session is minted;
839
+ * the user must sign in via {@link signInCustodial}.
840
+ *
841
+ * Mount this on the page declared as `verify_base_url` in your app's
842
+ * registration. Read `email` + `token` from `window.location.search`,
843
+ * call this, branch on `result.status`.
844
+ *
845
+ * Throws `auth_token_invalid_or_expired` if the token is wrong or
846
+ * past its 1h TTL — surface a "request a fresh link" CTA in that case.
847
+ */
848
+ async verifyEmail(input) {
849
+ if (!input.email || !input.token) {
850
+ throw new AithosSDKError("auth_invalid_input", "verifyEmail: email and token are required");
851
+ }
852
+ const resp = await custodialVerifyEmail({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
853
+ if (resp.status === "already_verified") {
854
+ return { status: "already_verified", email: resp.email };
855
+ }
856
+ // Magic-link sign-in path. Mirror `signInCustodial` to materialise
857
+ // the 4 sphere seeds in the keystore.
858
+ if (resp.seed.byteLength !== 128) {
859
+ zeroize(resp.seed);
860
+ zeroize(resp.encKey);
861
+ throw new AithosSDKError("auth_custodial_seed_format", `verifyEmail: expected 128-byte seed bundle, got ${resp.seed.byteLength}`);
862
+ }
863
+ const seedRoot = resp.seed.slice(0, 32);
864
+ const seedPublic = resp.seed.slice(32, 64);
865
+ const seedCircle = resp.seed.slice(64, 96);
866
+ const seedSelf = resp.seed.slice(96, 128);
867
+ const stored = {
868
+ version: "0.1.0-hex",
869
+ did: resp.did,
870
+ handle: resp.handle,
871
+ displayName: resp.displayName,
872
+ seedsHex: {
873
+ root: bytesToHex(seedRoot),
874
+ public: bytesToHex(seedPublic),
875
+ circle: bytesToHex(seedCircle),
876
+ self: bytesToHex(seedSelf),
877
+ },
878
+ savedAt: new Date().toISOString(),
879
+ };
880
+ zeroize(resp.seed);
881
+ zeroize(seedRoot);
882
+ zeroize(seedPublic);
883
+ zeroize(seedCircle);
884
+ zeroize(seedSelf);
885
+ zeroize(resp.encKey);
886
+ if (this.#ownerSigners)
887
+ this.#ownerSigners.destroy();
888
+ this.#ownerSigners = OwnerSigners.fromStoredOwnerKeys(stored);
889
+ await this.#keyStore.saveOwner(stored);
890
+ const session = {
891
+ session: resp.session,
892
+ exp: resp.exp,
893
+ did: resp.did,
894
+ handle: resp.handle,
895
+ blob_b64: bytesToB64Public(resp.blob),
896
+ blob_nonce_b64: bytesToB64Public(resp.blobNonce),
897
+ blob_version: resp.blobVersion,
898
+ enc_key_b64: "",
899
+ is_first_login: false,
900
+ };
901
+ this.#sessionStore.set(session);
902
+ return { status: "signed_in", session, passwordMustChange: false };
903
+ }
904
+ /**
905
+ * Re-send the verification mail for a pending account. Use when the
906
+ * user reports never having received the welcome mail, or when their
907
+ * verification token expired (24h TTL).
908
+ *
909
+ * The backend is anti-enumeration (always 200) and rate-limited
910
+ * 1/h/account, so it's safe to call even when the state of `email`
911
+ * is unknown. Accepts the same credential families as
912
+ * {@link signUpCustodial}; falls back to the constructor's
913
+ * `publicKey` when neither override is set.
914
+ */
915
+ async resendVerificationEmail(input) {
916
+ if (!input.email) {
917
+ throw new AithosSDKError("auth_invalid_input", "resendVerificationEmail: email is required");
918
+ }
919
+ const apiKey = input.apiKey;
920
+ const publicKey = input.publicKey ?? this.#publicKey;
921
+ if (!apiKey && !publicKey) {
922
+ throw new AithosSDKError("auth_missing_api_key", "resendVerificationEmail: pass apiKey, publicKey, or set publicKey on the AithosAuth constructor");
923
+ }
924
+ await custodialResendVerify({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, {
925
+ email: input.email,
926
+ ...(apiKey ? { apiKey } : {}),
927
+ ...(apiKey ? {} : publicKey ? { publicKey } : {}),
928
+ });
805
929
  }
806
930
  /**
807
931
  * Authenticate a custodial-mode user with email + password. Single
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0-alpha.30";
1
+ export declare const VERSION = "0.1.0-alpha.32";
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, ApplyPasswordResetInput, ApplyPasswordResetResult, CompleteSsoFirstLoginInput, CompleteSsoFirstLoginResult, CustodialSignInInput, CustodialSignInResult, CustodialSignUpInput, CustodialSignUpResult, DelegateInfo, ImportMandateInput, OwnerInfo, RequestPasswordResetInput, 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, ResendVerificationInput, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, VerifyEmailInput, VerifyEmailResult, } 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.30";
20
+ export const VERSION = "0.1.0-alpha.32";
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.30",
3
+ "version": "0.1.0-alpha.32",
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",