@aithos/sdk 0.1.0-alpha.31 → 0.1.0-alpha.33

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.
@@ -96,12 +96,42 @@ export interface CustodialVerifyEmailApiInput {
96
96
  readonly email: string;
97
97
  readonly token: string;
98
98
  }
99
- /** Consume the verification token from the confirmation link. Idempotent
100
- * on repeated clicks; throws `auth_token_invalid_or_expired` if the
101
- * token is wrong, consumed, or past its 24h TTL. */
102
- export declare function custodialVerifyEmail(http: HttpClient, input: CustodialVerifyEmailApiInput): Promise<{
103
- ok: true;
104
- }>;
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>;
105
135
  /** Re-send the verification mail for a pending account. The backend
106
136
  * is anti-enum (always 200) and rate-limited 1/h/account, so this is
107
137
  * safe to call even when the user state is unknown. Accepts the same
@@ -146,9 +146,15 @@ export async function custodialSignUp(http, input) {
146
146
  : {}),
147
147
  };
148
148
  }
149
- /** Consume the verification token from the confirmation link. Idempotent
150
- * on repeated clicks; throws `auth_token_invalid_or_expired` if the
151
- * token is wrong, consumed, or past its 24h TTL. */
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
+ */
152
158
  export async function custodialVerifyEmail(http, input) {
153
159
  const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/verify`, {
154
160
  method: "POST",
@@ -157,7 +163,25 @@ export async function custodialVerifyEmail(http, input) {
157
163
  });
158
164
  if (!res.ok)
159
165
  throw await readError(res, "custodial_verify_failed");
160
- return (await res.json());
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
+ };
161
185
  }
162
186
  /* ---- POST /auth/custodial/verify/resend -------------------------------- */
163
187
  /** Re-send the verification mail for a pending account. The backend
@@ -204,6 +204,25 @@ export interface VerifyEmailInput {
204
204
  readonly email: string;
205
205
  readonly token: string;
206
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
+ };
207
226
  /** Input to {@link AithosAuth.resendVerificationEmail}. The `email` is
208
227
  * required; credential overrides follow the same rules as
209
228
  * {@link CustodialSignUpInput}. */
@@ -403,18 +422,27 @@ export declare class AithosAuth {
403
422
  */
404
423
  signUpCustodial(input: CustodialSignUpInput): Promise<CustodialSignUpResult>;
405
424
  /**
406
- * Confirm the user's email address by consuming the one-time token
407
- * from the confirmation link. Idempotent on repeated clicks; throws
408
- * `auth_token_invalid_or_expired` if the token is wrong, already
409
- * consumed, or past its 24h TTL.
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}.
410
437
  *
411
438
  * Mount this on the page declared as `verify_base_url` in your app's
412
439
  * registration. Read `email` + `token` from `window.location.search`,
413
- * call this, then redirect to your sign-in page on success.
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.
414
444
  */
415
- verifyEmail(input: VerifyEmailInput): Promise<{
416
- ok: true;
417
- }>;
445
+ verifyEmail(input: VerifyEmailInput): Promise<VerifyEmailResult>;
418
446
  /**
419
447
  * Re-send the verification mail for a pending account. Use when the
420
448
  * user reports never having received the welcome mail, or when their
package/dist/src/auth.js CHANGED
@@ -20,7 +20,7 @@
20
20
  // JWT-less sessions (recovery / mandate sign-ins) are valid: the
21
21
  // keyStore is the source of truth for "is the user signed in", the
22
22
  // JWT is auxiliary for compute/wallet.
23
- import { buildBlobPlaintext, buildSignedEnvelope, createBrowserIdentity, decryptBlob, DEFAULT_KDF, deriveAuthAndEncKeys, encryptBlob, parseBlob, randomNonce, randomSalt, serializeBlob, signedDidDocument, zeroize, } from "@aithos/protocol-client";
23
+ import { browserIdentityFromStored, buildBlobPlaintext, buildSignedEnvelope, createBrowserIdentity, decryptBlob, DEFAULT_KDF, deriveAuthAndEncKeys, encryptBlob, parseBlob, randomNonce, randomSalt, serializeBlob, signedDidDocument, zeroize, } from "@aithos/protocol-client";
24
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";
@@ -825,20 +825,93 @@ export class AithosAuth {
825
825
  });
826
826
  }
827
827
  /**
828
- * Confirm the user's email address by consuming the one-time token
829
- * from the confirmation link. Idempotent on repeated clicks; throws
830
- * `auth_token_invalid_or_expired` if the token is wrong, already
831
- * consumed, or past its 24h TTL.
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}.
832
840
  *
833
841
  * Mount this on the page declared as `verify_base_url` in your app's
834
842
  * registration. Read `email` + `token` from `window.location.search`,
835
- * call this, then redirect to your sign-in page on success.
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.
836
847
  */
837
848
  async verifyEmail(input) {
838
849
  if (!input.email || !input.token) {
839
850
  throw new AithosSDKError("auth_invalid_input", "verifyEmail: email and token are required");
840
851
  }
841
- return custodialVerifyEmail({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, input);
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
+ // Bootstrap the Ethos on api.aithos.be (cf. notes in signInCustodial).
887
+ // The magic-link flow is the FIRST time the user actually has
888
+ // hydrated keys client-side, so this is typically when the identity
889
+ // gets published. Idempotent — safe to call again on subsequent
890
+ // clicks (which won't get here normally, but defensively).
891
+ const identity = browserIdentityFromStored({
892
+ handle: stored.handle,
893
+ displayName: stored.displayName,
894
+ did: stored.did,
895
+ seeds: stored.seedsHex,
896
+ });
897
+ await this.#publishIdentity(identity);
898
+ if (this.#ownerSigners)
899
+ this.#ownerSigners.destroy();
900
+ this.#ownerSigners = OwnerSigners.fromStoredOwnerKeys(stored);
901
+ await this.#keyStore.saveOwner(stored);
902
+ const session = {
903
+ session: resp.session,
904
+ exp: resp.exp,
905
+ did: resp.did,
906
+ handle: resp.handle,
907
+ blob_b64: bytesToB64Public(resp.blob),
908
+ blob_nonce_b64: bytesToB64Public(resp.blobNonce),
909
+ blob_version: resp.blobVersion,
910
+ enc_key_b64: "",
911
+ is_first_login: false,
912
+ };
913
+ this.#sessionStore.set(session);
914
+ return { status: "signed_in", session, passwordMustChange: false };
842
915
  }
843
916
  /**
844
917
  * Re-send the verification mail for a pending account. Use when the
@@ -928,6 +1001,24 @@ export class AithosAuth {
928
1001
  // The enc_key is informational here — the custodial blob is empty
929
1002
  // at first login. We still don't keep it in memory.
930
1003
  zeroize(resp.encKey);
1004
+ // Bootstrap the Ethos on api.aithos.be — same as signUp(zk). Without
1005
+ // this, the DID returned by signInCustodial isn't resolvable on the
1006
+ // platform (feed / profile lookups return "not found: did …"). The
1007
+ // call is idempotent server-side: a published identity replays as a
1008
+ // no-op. We do it here (rather than only on a "first login" flag)
1009
+ // because the auth Lambda doesn't know whether the api.aithos.be
1010
+ // side has been populated — the SDK is the single source of truth
1011
+ // for "the user's Ethos is bootstrapped".
1012
+ //
1013
+ // Failure aborts the sign-in: the user can retry (same behaviour as
1014
+ // signUp(zk)), and the local keystore is NOT populated half-way.
1015
+ const identity = browserIdentityFromStored({
1016
+ handle: stored.handle,
1017
+ displayName: stored.displayName,
1018
+ did: stored.did,
1019
+ seeds: stored.seedsHex,
1020
+ });
1021
+ await this.#publishIdentity(identity);
931
1022
  // Hydrate in-memory owner signers from the freshly-stored material.
932
1023
  if (this.#ownerSigners)
933
1024
  this.#ownerSigners.destroy();
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.0-alpha.31";
1
+ export declare const VERSION = "0.1.0-alpha.33";
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, ResendVerificationInput, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, VerifyEmailInput, } 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.31";
20
+ export const VERSION = "0.1.0-alpha.33";
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.31",
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.",
3
+ "version": "0.1.0-alpha.33",
4
+ "description": "Aithos SDK high-level TypeScript developer kit for building agentic apps on the Aithos protocol. Wraps @aithos/protocol-client and exposes the Aithos compute proxy and wallet (Stripe top-up) endpoints.",
5
5
  "keywords": [
6
6
  "aithos",
7
7
  "sdk",
@@ -39,15 +39,6 @@
39
39
  "README.md",
40
40
  "LICENSE"
41
41
  ],
42
- "scripts": {
43
- "build": "tsc",
44
- "build:test": "tsc -p tsconfig.test.json",
45
- "check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
46
- "test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
47
- "test:watch": "cd dist && node --test --watch",
48
- "clean": "rm -rf dist",
49
- "prepublishOnly": "npm run clean && npm run build && npm test"
50
- },
51
42
  "engines": {
52
43
  "node": ">=20"
53
44
  },
@@ -63,5 +54,13 @@
63
54
  "publishConfig": {
64
55
  "access": "public",
65
56
  "tag": "alpha"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc",
60
+ "build:test": "tsc -p tsconfig.test.json",
61
+ "check-types": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
62
+ "test": "npm run clean && npm run build && npm run build:test && cd dist && node --test",
63
+ "test:watch": "cd dist && node --test --watch",
64
+ "clean": "rm -rf dist"
66
65
  }
67
- }
66
+ }