@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.
- package/dist/src/auth-api.d.ts +9 -0
- package/dist/src/auth-api.js +20 -0
- package/dist/src/auth.d.ts +54 -0
- package/dist/src/auth.js +176 -29
- package/dist/src/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/src/auth-api.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/auth-api.js
CHANGED
|
@@ -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 {
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.#
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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() {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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.
|
|
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",
|