@aithos/sdk 0.1.0-alpha.12 → 0.1.0-alpha.13

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.
@@ -21,6 +21,15 @@ export interface RegisterApiResponse {
21
21
  readonly exp: number;
22
22
  }
23
23
  export declare function registerAccount(http: HttpClient, input: RegisterApiInput): Promise<RegisterApiResponse>;
24
+ export interface PutBlobApiInput {
25
+ readonly jwt: string;
26
+ readonly blob: Uint8Array;
27
+ readonly blobNonce: Uint8Array;
28
+ readonly blobVersion: number;
29
+ }
30
+ export declare function putBlob(http: HttpClient, input: PutBlobApiInput): Promise<{
31
+ ok: true;
32
+ }>;
24
33
  export interface LoginChallengeResponse {
25
34
  readonly authSalt: Uint8Array;
26
35
  readonly encSalt: Uint8Array;
@@ -41,6 +41,19 @@ async function postJson(http, path, body, jwt) {
41
41
  throw await readError(res, "request_failed");
42
42
  return (await res.json());
43
43
  }
44
+ async function putJson(http, path, body, jwt) {
45
+ const res = await http.fetchImpl(`${http.authBaseUrl}${path}`, {
46
+ method: "PUT",
47
+ headers: {
48
+ "content-type": "application/json",
49
+ authorization: `Bearer ${jwt}`,
50
+ },
51
+ body: JSON.stringify(body),
52
+ });
53
+ if (!res.ok)
54
+ throw await readError(res, "request_failed");
55
+ return (await res.json());
56
+ }
44
57
  export async function registerAccount(http, input) {
45
58
  return postJson(http, "/auth/register", {
46
59
  email: input.email,
@@ -56,6 +69,13 @@ export async function registerAccount(http, input) {
56
69
  blob_version: input.blobVersion,
57
70
  });
58
71
  }
72
+ export async function putBlob(http, input) {
73
+ return putJson(http, "/auth/blob", {
74
+ blob_b64: bytesToB64(input.blob),
75
+ blob_nonce_b64: bytesToB64(input.blobNonce),
76
+ blob_version: input.blobVersion,
77
+ }, input.jwt);
78
+ }
59
79
  export async function loginChallenge(http, email) {
60
80
  const wire = await postJson(http, "/auth/login/challenge", { email });
61
81
  return {
@@ -105,6 +105,28 @@ export interface SignUpResult {
105
105
  readonly recoveryFile: Blob;
106
106
  readonly recoveryFilename: string;
107
107
  }
108
+ /**
109
+ * Input to {@link AithosAuth.completeSsoFirstLogin}. The handle is
110
+ * required (the auth backend pre-generated one from the user's email
111
+ * local-part, available on the session payload — we re-confirm it
112
+ * here so the user can edit before commit).
113
+ */
114
+ export interface CompleteSsoFirstLoginInput {
115
+ readonly handle: string;
116
+ readonly displayName?: string;
117
+ }
118
+ /**
119
+ * Result of {@link AithosAuth.completeSsoFirstLogin}. Returns a recovery
120
+ * file just like signUp — even though the user authenticated via Google,
121
+ * the freshly-generated Ed25519 seeds are the only material that can
122
+ * sign Aithos artifacts; without the recovery file, losing access to
123
+ * the Google account means losing the ethos forever.
124
+ */
125
+ export interface CompleteSsoFirstLoginResult {
126
+ readonly session: AithosSession;
127
+ readonly recoveryFile: Blob;
128
+ readonly recoveryFilename: string;
129
+ }
108
130
  export interface SignInWithRecoveryInput {
109
131
  /** Recovery file as a Blob (browser File input) or already-decoded JSON string. */
110
132
  readonly file: Blob | string;
@@ -185,6 +207,38 @@ export declare class AithosAuth {
185
207
  signInWithGoogle(opts?: SignInWithGoogleOptions): never;
186
208
  handleCallback(): Promise<AithosSession | null>;
187
209
  exchange(aithosCode: string): Promise<AithosSession>;
210
+ /**
211
+ * Finish the first-time Google SSO bootstrap. After
212
+ * `signInWithGoogle()` + `handleCallback()`, a brand-new SSO user has
213
+ * a session JWT and an `enc_key` released by the auth backend, but
214
+ * NO Aithos identity yet (no Ed25519 seeds, no published did.json,
215
+ * no blob in the auth vault). This method closes that gap:
216
+ *
217
+ * 1. Generates a fresh {@link BrowserIdentity} client-side (4
218
+ * Ed25519 keypairs, derived DID).
219
+ * 2. Calls `aithos.publish_identity` on api.aithos.be so reads
220
+ * and writes against the Aithos primitives have an ethos to
221
+ * anchor to.
222
+ * 3. AES-GCM-encrypts the seeds with the session's `enc_key`,
223
+ * PUTs the result to `/auth/blob`. From now on, every Google
224
+ * sign-in for this user will receive the encrypted blob and
225
+ * hydrate locally.
226
+ * 4. Hydrates `ownerSigners` + `keyStore` so `canSignAsOwner()`
227
+ * flips to true.
228
+ * 5. Returns a recovery-file Blob — the only material that can
229
+ * restore this ethos if Google access is lost.
230
+ *
231
+ * Preconditions:
232
+ * - `getCurrentSession()` returns a non-null session (caller went
233
+ * through `handleCallback()` already).
234
+ * - The session's `blob_version` is 0 (i.e. no blob yet).
235
+ * - The session's `enc_key_b64` is non-empty.
236
+ *
237
+ * Throws `AithosSDKError("auth_sso_no_pending_first_login", …)` if
238
+ * preconditions don't hold (e.g. blob_version > 0 means the user has
239
+ * already completed setup; nothing to do).
240
+ */
241
+ completeSsoFirstLogin(input: CompleteSsoFirstLoginInput): Promise<CompleteSsoFirstLoginResult>;
188
242
  signOut(): Promise<void>;
189
243
  }
190
244
  //# 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, registerAccount, } from "./auth-api.js";
24
+ import { 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";
@@ -504,34 +504,44 @@ export class AithosAuth {
504
504
  const blobBytes = decryptBlob(encKey, nonce, blob);
505
505
  try {
506
506
  const plaintext = parseBlob(blobBytes);
507
- if (plaintext.identity.did === session.did) {
508
- if (this.#ownerSigners)
509
- this.#ownerSigners.destroy();
510
- this.#ownerSigners = OwnerSigners.fromBlobPlaintext(plaintext);
511
- await this.#keyStore.saveOwner({
512
- version: "0.1.0-hex",
513
- did: plaintext.identity.did,
514
- handle: plaintext.identity.handle,
515
- displayName: plaintext.identity.displayName,
516
- seedsHex: plaintext.seeds,
517
- savedAt: new Date().toISOString(),
518
- });
519
- await this.#keyStore.clearAllDelegates();
520
- this.#delegates.destroy();
521
- for (const d of plaintext.delegates) {
522
- const stored = storedDelegateFromBlob(d);
523
- try {
524
- await this.#keyStore.saveDelegate(stored);
525
- }
526
- catch {
527
- /* keep going */
528
- }
529
- try {
530
- this.#delegates.add(DelegateActor.fromStored(stored));
531
- }
532
- catch {
533
- /* keep going */
534
- }
507
+ // Earlier versions of the SDK gated hydration on
508
+ // `plaintext.identity.did === session.did` as a defense
509
+ // against tampered sessionStores. The check breaks SSO
510
+ // flows: the auth backend assigns a placeholder random
511
+ // DID at user-record creation time (no client keypair on
512
+ // hand), but the BLOB is built around a real
513
+ // BrowserIdentity whose DID is derived from its root
514
+ // pubkey. The two intentionally differ — the blob is the
515
+ // truth source for everything downstream (signing, DID
516
+ // resolution against api.aithos.be), the session.did is
517
+ // just auth-side bookkeeping. Drop the check and trust
518
+ // the blob.
519
+ if (this.#ownerSigners)
520
+ this.#ownerSigners.destroy();
521
+ this.#ownerSigners = OwnerSigners.fromBlobPlaintext(plaintext);
522
+ await this.#keyStore.saveOwner({
523
+ version: "0.1.0-hex",
524
+ did: plaintext.identity.did,
525
+ handle: plaintext.identity.handle,
526
+ displayName: plaintext.identity.displayName,
527
+ seedsHex: plaintext.seeds,
528
+ savedAt: new Date().toISOString(),
529
+ });
530
+ await this.#keyStore.clearAllDelegates();
531
+ this.#delegates.destroy();
532
+ for (const d of plaintext.delegates) {
533
+ const stored = storedDelegateFromBlob(d);
534
+ try {
535
+ await this.#keyStore.saveDelegate(stored);
536
+ }
537
+ catch {
538
+ /* keep going */
539
+ }
540
+ try {
541
+ this.#delegates.add(DelegateActor.fromStored(stored));
542
+ }
543
+ catch {
544
+ /* keep going */
535
545
  }
536
546
  }
537
547
  }
@@ -580,6 +590,143 @@ export class AithosAuth {
580
590
  return (await res.json());
581
591
  }
582
592
  /* ------------------------------------------------------------------------ */
593
+ /* Complete SSO first login */
594
+ /* ------------------------------------------------------------------------ */
595
+ /**
596
+ * Finish the first-time Google SSO bootstrap. After
597
+ * `signInWithGoogle()` + `handleCallback()`, a brand-new SSO user has
598
+ * a session JWT and an `enc_key` released by the auth backend, but
599
+ * NO Aithos identity yet (no Ed25519 seeds, no published did.json,
600
+ * no blob in the auth vault). This method closes that gap:
601
+ *
602
+ * 1. Generates a fresh {@link BrowserIdentity} client-side (4
603
+ * Ed25519 keypairs, derived DID).
604
+ * 2. Calls `aithos.publish_identity` on api.aithos.be so reads
605
+ * and writes against the Aithos primitives have an ethos to
606
+ * anchor to.
607
+ * 3. AES-GCM-encrypts the seeds with the session's `enc_key`,
608
+ * PUTs the result to `/auth/blob`. From now on, every Google
609
+ * sign-in for this user will receive the encrypted blob and
610
+ * hydrate locally.
611
+ * 4. Hydrates `ownerSigners` + `keyStore` so `canSignAsOwner()`
612
+ * flips to true.
613
+ * 5. Returns a recovery-file Blob — the only material that can
614
+ * restore this ethos if Google access is lost.
615
+ *
616
+ * Preconditions:
617
+ * - `getCurrentSession()` returns a non-null session (caller went
618
+ * through `handleCallback()` already).
619
+ * - The session's `blob_version` is 0 (i.e. no blob yet).
620
+ * - The session's `enc_key_b64` is non-empty.
621
+ *
622
+ * Throws `AithosSDKError("auth_sso_no_pending_first_login", …)` if
623
+ * preconditions don't hold (e.g. blob_version > 0 means the user has
624
+ * already completed setup; nothing to do).
625
+ */
626
+ async completeSsoFirstLogin(input) {
627
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/i.test(input.handle)) {
628
+ throw new AithosSDKError("auth_invalid_handle", "handle must be 1–63 alphanumeric chars + _ -");
629
+ }
630
+ const displayName = input.displayName ?? input.handle;
631
+ const session = this.#sessionStore.get();
632
+ if (!session) {
633
+ throw new AithosSDKError("auth_sso_no_pending_first_login", "no active session — sign in via Google first");
634
+ }
635
+ if (!session.enc_key_b64) {
636
+ throw new AithosSDKError("auth_sso_no_pending_first_login", "session does not carry an enc_key (not an SSO-flow session?)");
637
+ }
638
+ if (session.blob_version > 0) {
639
+ throw new AithosSDKError("auth_sso_no_pending_first_login", "this session already has a published blob — nothing to bootstrap");
640
+ }
641
+ // 1. Fresh identity client-side. The DID derived here is the
642
+ // truth source from now on — the placeholder DID stamped in
643
+ // the user record by the auth Lambda is left as-is (auth-side
644
+ // bookkeeping; never used for signing).
645
+ const identity = createBrowserIdentity(input.handle, displayName);
646
+ const recoverySerialized = serializeRecoveryFile(identity);
647
+ const recoveryFile = new Blob([recoverySerialized.text], {
648
+ type: "application/json",
649
+ });
650
+ // 2. publish_identity on api.aithos.be — reuses the alpha.6
651
+ // helper. Must succeed before we persist anything locally:
652
+ // a half-completed bootstrap (blob uploaded but identity not
653
+ // published) would leave the user with seeds they can't use.
654
+ await this.#publishIdentity(identity);
655
+ // 3. Encrypt the seeds with the SSO-released enc_key and PUT
656
+ // /auth/blob. The auth Lambda accepts the new blob_version=1
657
+ // and stores the bytes verbatim.
658
+ const encKey = b64ToBytes(session.enc_key_b64);
659
+ let blob;
660
+ let blobNonce;
661
+ let plaintext;
662
+ try {
663
+ plaintext = buildBlobPlaintext({
664
+ identity: {
665
+ did: identity.did,
666
+ handle: identity.handle,
667
+ displayName: identity.displayName,
668
+ },
669
+ seeds: {
670
+ root: identity.root.seed,
671
+ public: identity.public.seed,
672
+ circle: identity.circle.seed,
673
+ self: identity.self.seed,
674
+ },
675
+ delegates: [],
676
+ });
677
+ const blobBytes = serializeBlob(plaintext);
678
+ blobNonce = randomNonce();
679
+ blob = encryptBlob(encKey, blobNonce, blobBytes);
680
+ }
681
+ finally {
682
+ zeroize(encKey);
683
+ }
684
+ const newBlobVersion = 1;
685
+ try {
686
+ await putBlob({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, {
687
+ jwt: session.session,
688
+ blob,
689
+ blobNonce,
690
+ blobVersion: newBlobVersion,
691
+ });
692
+ }
693
+ catch (e) {
694
+ throw new AithosSDKError("auth_sso_blob_upload_failed", `couldn't store the encrypted vault on auth.aithos.be: ${e.message ?? "unknown"}`);
695
+ }
696
+ // 4. Hydrate in-memory state from the fresh identity.
697
+ if (this.#ownerSigners)
698
+ this.#ownerSigners.destroy();
699
+ this.#ownerSigners = OwnerSigners.fromBrowserIdentity(identity);
700
+ await this.#keyStore.saveOwner({
701
+ version: "0.1.0-hex",
702
+ did: identity.did,
703
+ handle: identity.handle,
704
+ displayName: identity.displayName,
705
+ seedsHex: {
706
+ root: bytesToHex(identity.root.seed),
707
+ public: bytesToHex(identity.public.seed),
708
+ circle: bytesToHex(identity.circle.seed),
709
+ self: bytesToHex(identity.self.seed),
710
+ },
711
+ savedAt: new Date().toISOString(),
712
+ });
713
+ // 5. Persist the updated session — same JWT, but now carrying
714
+ // the freshly-built blob bytes so a subsequent `resume()` can
715
+ // rehydrate without another /auth/blob round-trip.
716
+ const refreshed = {
717
+ ...session,
718
+ blob_b64: bytesToB64Public(blob),
719
+ blob_nonce_b64: bytesToB64Public(blobNonce),
720
+ blob_version: newBlobVersion,
721
+ };
722
+ this.#sessionStore.set(refreshed);
723
+ return {
724
+ session: refreshed,
725
+ recoveryFile,
726
+ recoveryFilename: recoverySerialized.filename,
727
+ };
728
+ }
729
+ /* ------------------------------------------------------------------------ */
583
730
  /* Sign-out */
584
731
  /* ------------------------------------------------------------------------ */
585
732
  async signOut() {
@@ -10,7 +10,7 @@ export { ComputeNamespace } from "./compute.js";
10
10
  export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
11
11
  export { WalletNamespace } from "./wallet.js";
12
12
  export { AithosAuth, DEFAULT_API_BASE_URL, DEFAULT_AUTH_BASE_URL, } from "./auth.js";
13
- export type { AithosAuthConfig, AithosSession, DelegateInfo, ImportMandateInput, OwnerInfo, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, } from "./auth.js";
13
+ export type { AithosAuthConfig, AithosSession, CompleteSsoFirstLoginInput, CompleteSsoFirstLoginResult, DelegateInfo, ImportMandateInput, OwnerInfo, SignInInput, SignInWithGoogleOptions, SignInWithRecoveryInput, SignUpInput, SignUpResult, } from "./auth.js";
14
14
  export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, type AithosSessionStore, } from "./session-store.js";
15
15
  export { DEFAULT_KEYSTORE_DB_NAME, defaultKeyStore, indexedDbKeyStore, memoryKeyStore, type AithosKeyStore, type StoredDelegateKeys, type StoredOwnerKeys, } from "./key-store.js";
16
16
  export { EthosClient, EthosNamespace, EthosZone, ZONE_NAMES, } from "./ethos.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aithos/sdk",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
4
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",