@aithos/sdk 0.1.0-alpha.3 → 0.1.0-alpha.4
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 +41 -0
- package/dist/src/auth-api.js +82 -0
- package/dist/src/auth.d.ts +107 -26
- package/dist/src/auth.js +235 -36
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +10 -4
- package/dist/src/session-store.d.ts +58 -0
- package/dist/src/session-store.js +158 -0
- package/package.json +3 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type KdfParams } from "@aithos/protocol-client";
|
|
2
|
+
interface HttpClient {
|
|
3
|
+
readonly fetchImpl: typeof fetch;
|
|
4
|
+
readonly authBaseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RegisterApiInput {
|
|
7
|
+
readonly email: string;
|
|
8
|
+
readonly handle: string;
|
|
9
|
+
readonly displayName: string;
|
|
10
|
+
readonly did: string;
|
|
11
|
+
readonly authKey: Uint8Array;
|
|
12
|
+
readonly authSalt: Uint8Array;
|
|
13
|
+
readonly encSalt: Uint8Array;
|
|
14
|
+
readonly kdf: KdfParams;
|
|
15
|
+
readonly blob: Uint8Array;
|
|
16
|
+
readonly blobNonce: Uint8Array;
|
|
17
|
+
readonly blobVersion: number;
|
|
18
|
+
}
|
|
19
|
+
export interface RegisterApiResponse {
|
|
20
|
+
readonly session: string;
|
|
21
|
+
readonly exp: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function registerAccount(http: HttpClient, input: RegisterApiInput): Promise<RegisterApiResponse>;
|
|
24
|
+
export interface LoginChallengeResponse {
|
|
25
|
+
readonly authSalt: Uint8Array;
|
|
26
|
+
readonly encSalt: Uint8Array;
|
|
27
|
+
readonly kdf: KdfParams;
|
|
28
|
+
}
|
|
29
|
+
export declare function loginChallenge(http: HttpClient, email: string): Promise<LoginChallengeResponse>;
|
|
30
|
+
export interface LoginVerifyResponse {
|
|
31
|
+
readonly session: string;
|
|
32
|
+
readonly exp: number;
|
|
33
|
+
readonly did: string;
|
|
34
|
+
readonly handle: string;
|
|
35
|
+
readonly blob: Uint8Array;
|
|
36
|
+
readonly blobNonce: Uint8Array;
|
|
37
|
+
readonly blobVersion: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function loginVerify(http: HttpClient, email: string, authKey: Uint8Array): Promise<LoginVerifyResponse>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=auth-api.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
// Thin HTTP client over the Aithos auth Lambda.
|
|
4
|
+
//
|
|
5
|
+
// Internal — not exported from the package's public surface. The
|
|
6
|
+
// {@link AithosAuth} class composes these calls with the crypto
|
|
7
|
+
// primitives in `@aithos/protocol-client` and the session store in
|
|
8
|
+
// {@link ./session-store.ts} to expose a high-level
|
|
9
|
+
// signIn / signUp / signInWithGoogle API.
|
|
10
|
+
//
|
|
11
|
+
// Wire format mirrors `aithos/auth/API.md` exactly. All `*_b64` fields
|
|
12
|
+
// are standard-base64 (not URL-safe), padding stripped — see
|
|
13
|
+
// `bytesToB64` / `b64ToBytes` in `@aithos/protocol-client`.
|
|
14
|
+
import { bytesToB64, b64ToBytes, } from "@aithos/protocol-client";
|
|
15
|
+
import { AithosSDKError } from "./types.js";
|
|
16
|
+
async function readError(res, defaultCode) {
|
|
17
|
+
let body = null;
|
|
18
|
+
try {
|
|
19
|
+
body = (await res.json());
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* body not JSON */
|
|
23
|
+
}
|
|
24
|
+
const code = typeof body?.code === "string" ? `auth_${body.code}` : `auth_${defaultCode}`;
|
|
25
|
+
const message = body?.error ?? `${res.status} ${res.statusText || "request failed"}`;
|
|
26
|
+
return new AithosSDKError(code, message, {
|
|
27
|
+
status: res.status,
|
|
28
|
+
...(body !== null ? { data: body } : {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function postJson(http, path, body, jwt) {
|
|
32
|
+
const res = await http.fetchImpl(`${http.authBaseUrl}${path}`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": "application/json",
|
|
36
|
+
...(jwt ? { authorization: `Bearer ${jwt}` } : {}),
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify(body),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok)
|
|
41
|
+
throw await readError(res, "request_failed");
|
|
42
|
+
return (await res.json());
|
|
43
|
+
}
|
|
44
|
+
export async function registerAccount(http, input) {
|
|
45
|
+
return postJson(http, "/auth/register", {
|
|
46
|
+
email: input.email,
|
|
47
|
+
handle: input.handle,
|
|
48
|
+
display_name: input.displayName,
|
|
49
|
+
did: input.did,
|
|
50
|
+
auth_key_b64: bytesToB64(input.authKey),
|
|
51
|
+
auth_salt_b64: bytesToB64(input.authSalt),
|
|
52
|
+
enc_salt_b64: bytesToB64(input.encSalt),
|
|
53
|
+
kdf: input.kdf,
|
|
54
|
+
blob_b64: bytesToB64(input.blob),
|
|
55
|
+
blob_nonce_b64: bytesToB64(input.blobNonce),
|
|
56
|
+
blob_version: input.blobVersion,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function loginChallenge(http, email) {
|
|
60
|
+
const wire = await postJson(http, "/auth/login/challenge", { email });
|
|
61
|
+
return {
|
|
62
|
+
authSalt: b64ToBytes(wire.auth_salt_b64),
|
|
63
|
+
encSalt: b64ToBytes(wire.enc_salt_b64),
|
|
64
|
+
kdf: wire.kdf,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function loginVerify(http, email, authKey) {
|
|
68
|
+
const wire = await postJson(http, "/auth/login/verify", {
|
|
69
|
+
email,
|
|
70
|
+
auth_key_b64: bytesToB64(authKey),
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
session: wire.session,
|
|
74
|
+
exp: wire.exp,
|
|
75
|
+
did: wire.did,
|
|
76
|
+
handle: wire.handle,
|
|
77
|
+
blob: b64ToBytes(wire.blob_b64),
|
|
78
|
+
blobNonce: b64ToBytes(wire.blob_nonce_b64),
|
|
79
|
+
blobVersion: wire.blob_version,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=auth-api.js.map
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AithosSessionStore } from "./session-store.js";
|
|
1
2
|
/** Default URL of the Aithos auth backend. */
|
|
2
3
|
export declare const DEFAULT_AUTH_BASE_URL = "https://auth.aithos.be";
|
|
3
4
|
/**
|
|
@@ -21,14 +22,20 @@ export interface AithosAuthConfig {
|
|
|
21
22
|
* shimming jsdom.
|
|
22
23
|
*/
|
|
23
24
|
readonly window?: Pick<Window, "location" | "history">;
|
|
25
|
+
/**
|
|
26
|
+
* Pluggable session storage. Defaults to {@link sessionStorageStore}
|
|
27
|
+
* in browser environments, {@link noopStore} elsewhere. See
|
|
28
|
+
* `./session-store.ts` for built-in alternatives or to roll your own.
|
|
29
|
+
*/
|
|
30
|
+
readonly sessionStore?: AithosSessionStore;
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
|
-
*
|
|
33
|
+
* Active Aithos session. Persisted in the configured session store and
|
|
34
|
+
* surfaced by {@link AithosAuth.getCurrentSession}.
|
|
27
35
|
*
|
|
28
|
-
* Wire-compatible with the auth Lambda's `SsoExchangeResponse
|
|
29
|
-
* are kept snake_case to match
|
|
30
|
-
*
|
|
31
|
-
* Network panel.
|
|
36
|
+
* Wire-compatible with the auth Lambda's `SsoExchangeResponse` and
|
|
37
|
+
* register/verify payloads ; field names are kept snake_case to match
|
|
38
|
+
* the backend.
|
|
32
39
|
*/
|
|
33
40
|
export interface AithosSession {
|
|
34
41
|
/** HS256 JWT — send in `Authorization: Bearer <session>` to auth/* and
|
|
@@ -40,21 +47,20 @@ export interface AithosSession {
|
|
|
40
47
|
readonly did: string;
|
|
41
48
|
/** User-visible handle (rendered as `@handle`). */
|
|
42
49
|
readonly handle: string;
|
|
43
|
-
/** Encrypted vault, base64. Empty string
|
|
50
|
+
/** Encrypted vault blob, base64. Empty string on first Google sign-in. */
|
|
44
51
|
readonly blob_b64: string;
|
|
45
|
-
/** AES-GCM nonce for the blob, base64 (12 bytes). Empty on first sign-in. */
|
|
52
|
+
/** AES-GCM nonce for the blob, base64 (12 bytes). Empty on first Google sign-in. */
|
|
46
53
|
readonly blob_nonce_b64: string;
|
|
47
|
-
/** Monotonic blob version.
|
|
54
|
+
/** Monotonic blob version. 0 on first Google sign-in. */
|
|
48
55
|
readonly blob_version: number;
|
|
49
|
-
/** 32-byte vault key, base64. Decrypts {@link blob_b64} via AES-GCM-256.
|
|
56
|
+
/** 32-byte vault key, base64. Decrypts {@link blob_b64} via AES-GCM-256.
|
|
57
|
+
* Returned by Google SSO ; absent (empty string) for password sign-in
|
|
58
|
+
* where the key stays in browser memory only. */
|
|
50
59
|
readonly enc_key_b64: string;
|
|
51
|
-
/** True the first time this user signs in
|
|
52
|
-
* onboarding flow rather than mounting an empty blob. */
|
|
60
|
+
/** True the first time this user signs in (Google flow only). */
|
|
53
61
|
readonly is_first_login: boolean;
|
|
54
62
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Options for {@link AithosAuth.signInWithGoogle}.
|
|
57
|
-
*/
|
|
63
|
+
/** Options for {@link AithosAuth.signInWithGoogle}. */
|
|
58
64
|
export interface SignInWithGoogleOptions {
|
|
59
65
|
/**
|
|
60
66
|
* Opaque deep-link state preserved across the OAuth round-trip and
|
|
@@ -66,50 +72,125 @@ export interface SignInWithGoogleOptions {
|
|
|
66
72
|
*/
|
|
67
73
|
readonly appState?: string;
|
|
68
74
|
}
|
|
75
|
+
/** Options for {@link AithosAuth.signIn}. */
|
|
76
|
+
export interface SignInInput {
|
|
77
|
+
readonly email: string;
|
|
78
|
+
readonly password: string;
|
|
79
|
+
}
|
|
80
|
+
/** Options for {@link AithosAuth.signUp}. */
|
|
81
|
+
export interface SignUpInput {
|
|
82
|
+
readonly email: string;
|
|
83
|
+
readonly password: string;
|
|
84
|
+
/** Aithos handle (the @-name). 1–63 alphanumeric chars + `_`/`-`. */
|
|
85
|
+
readonly handle: string;
|
|
86
|
+
/** Optional human-readable name. Defaults to the handle. */
|
|
87
|
+
readonly displayName?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Result of {@link AithosAuth.signUp}. */
|
|
90
|
+
export interface SignUpResult {
|
|
91
|
+
readonly session: AithosSession;
|
|
92
|
+
/**
|
|
93
|
+
* Recovery file containing the user's seed material, plaintext at the
|
|
94
|
+
* V1 spec ; the app should offer this to the user as a download. Lose
|
|
95
|
+
* this file AND forget the password = lose the ethos.
|
|
96
|
+
*
|
|
97
|
+
* Format : JSON, content-type `application/json`. Filename suggestion
|
|
98
|
+
* exposed as {@link recoveryFilename} for direct use with `<a download>`.
|
|
99
|
+
*/
|
|
100
|
+
readonly recoveryFile: Blob;
|
|
101
|
+
/** Suggested filename for {@link recoveryFile}. */
|
|
102
|
+
readonly recoveryFilename: string;
|
|
103
|
+
}
|
|
69
104
|
/**
|
|
70
105
|
* Authenticator for the Aithos identity service. One instance per app
|
|
71
|
-
* is the recommended pattern (the constructor is cheap
|
|
72
|
-
*
|
|
106
|
+
* is the recommended pattern (the constructor is cheap).
|
|
107
|
+
*
|
|
108
|
+
* The class is **stateful** in one specific way : it owns a session store
|
|
109
|
+
* that gets written on every successful auth call and read by
|
|
110
|
+
* {@link getCurrentSession}. Pass a custom store at construction time
|
|
111
|
+
* if you need different persistence (localStorage, IndexedDB, no-op).
|
|
73
112
|
*/
|
|
74
113
|
export declare class AithosAuth {
|
|
75
114
|
/** Resolved auth base URL with a trailing slash trimmed. */
|
|
76
115
|
readonly authBaseUrl: string;
|
|
77
116
|
private readonly fetchImpl;
|
|
78
117
|
private readonly win;
|
|
118
|
+
private readonly store;
|
|
79
119
|
constructor(config?: AithosAuthConfig);
|
|
120
|
+
/**
|
|
121
|
+
* Sign in to an existing Aithos account. Two-round-trip flow under the
|
|
122
|
+
* hood :
|
|
123
|
+
*
|
|
124
|
+
* 1. POST /auth/login/challenge → server returns the salts + KDF params
|
|
125
|
+
* 2. derive auth_key from password + salt (Argon2id)
|
|
126
|
+
* 3. POST /auth/login/verify → server checks auth_key, returns JWT + blob
|
|
127
|
+
*
|
|
128
|
+
* The returned `enc_key_b64` is empty by design : password sign-in
|
|
129
|
+
* doesn't release the vault key over the wire (it's derived locally
|
|
130
|
+
* but discarded after the call). Apps that need the seeds — most
|
|
131
|
+
* apps don't — should use the (forthcoming) `loadEthos` helper
|
|
132
|
+
* separately with the password still in hand.
|
|
133
|
+
*
|
|
134
|
+
* Persists the session in the configured store before returning.
|
|
135
|
+
*/
|
|
136
|
+
signIn(input: SignInInput): Promise<AithosSession>;
|
|
137
|
+
/**
|
|
138
|
+
* Create a new Aithos account end-to-end :
|
|
139
|
+
*
|
|
140
|
+
* 1. Generate a fresh `BrowserIdentity` (4 Ed25519/X25519 seeds)
|
|
141
|
+
* 2. Build the recovery file (plaintext JSON, the user must save it)
|
|
142
|
+
* 3. Derive auth_key + enc_key from the password (Argon2id, fresh salts)
|
|
143
|
+
* 4. Encrypt the seeds in a vault blob (AES-GCM-256)
|
|
144
|
+
* 5. POST /auth/register with everything → JWT
|
|
145
|
+
* 6. Persist the session and return it + the recovery Blob
|
|
146
|
+
*
|
|
147
|
+
* The seeds are NOT published as an Aithos ethos here : the user's
|
|
148
|
+
* profile on `app.aithos.be` won't appear until they (or another app)
|
|
149
|
+
* publishes their first edition. This matches `aithos/app`'s design,
|
|
150
|
+
* where the vault is the source of truth for keys and the published
|
|
151
|
+
* edition is a separate concern.
|
|
152
|
+
*/
|
|
153
|
+
signUp(input: SignUpInput): Promise<SignUpResult>;
|
|
80
154
|
/**
|
|
81
155
|
* Redirect the browser to Google's OAuth consent screen. Must be called
|
|
82
156
|
* synchronously in response to a user gesture (button click) — most
|
|
83
157
|
* browsers block top-level navigation triggered from idle code.
|
|
84
158
|
*
|
|
85
|
-
* Does not return: navigation tears the JS context down. The `never`
|
|
159
|
+
* Does not return : navigation tears the JS context down. The `never`
|
|
86
160
|
* return type tells callers any code after the call is unreachable.
|
|
87
161
|
*/
|
|
88
162
|
signInWithGoogle(opts?: SignInWithGoogleOptions): never;
|
|
89
163
|
/**
|
|
90
164
|
* Inspect the current URL for an `aithos_code` query parameter. If it's
|
|
91
|
-
* present, exchange it at the backend and return
|
|
92
|
-
*
|
|
165
|
+
* present, exchange it at the backend, persist the session, and return
|
|
166
|
+
* it. The query params are stripped from the URL via
|
|
93
167
|
* `history.replaceState` so a page refresh doesn't replay the redeem
|
|
94
168
|
* (which would 410 anyway).
|
|
95
169
|
*
|
|
96
170
|
* Returns `null` when there's no code in the URL — safe to call on every
|
|
97
171
|
* page load. Throws {@link AithosSDKError} on backend errors or when
|
|
98
|
-
* the URL carries `aithos_error
|
|
99
|
-
* failure, etc.).
|
|
172
|
+
* the URL carries `aithos_error=…`.
|
|
100
173
|
*/
|
|
101
174
|
handleCallback(): Promise<AithosSession | null>;
|
|
102
175
|
/**
|
|
103
176
|
* Programmatically redeem an `aithos_code` for a session. `handleCallback`
|
|
104
|
-
* calls this for you; expose it directly for callers that already pulled
|
|
177
|
+
* calls this for you ; expose it directly for callers that already pulled
|
|
105
178
|
* the code out of the URL via their own router.
|
|
179
|
+
*
|
|
180
|
+
* Note : this method does NOT persist the session — it's the lower-level
|
|
181
|
+
* primitive. Use `handleCallback` for the full pipe.
|
|
106
182
|
*/
|
|
107
183
|
exchange(aithosCode: string): Promise<AithosSession>;
|
|
108
184
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
185
|
+
* Read the active session from the configured store. Returns null if
|
|
186
|
+
* the user is signed out, or if the JWT has expired (the store
|
|
187
|
+
* auto-evicts expired entries — see ./session-store.ts).
|
|
188
|
+
*/
|
|
189
|
+
getCurrentSession(): AithosSession | null;
|
|
190
|
+
/**
|
|
191
|
+
* Stateless sign-out — the Aithos backend doesn't track sessions, so
|
|
192
|
+
* there's nothing to revoke server-side ; this method clears the
|
|
193
|
+
* configured session store and resolves.
|
|
113
194
|
*/
|
|
114
195
|
signOut(): Promise<void>;
|
|
115
196
|
}
|
package/dist/src/auth.js
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright 2026 Mathieu Colla
|
|
3
|
-
//
|
|
3
|
+
// Aithos auth — sign-up, sign-in, sign-in-with-Google.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// what the flow returns (via the encrypted blob the response carries). So
|
|
8
|
-
// we expose a standalone {@link AithosAuth} class that talks to the Aithos
|
|
9
|
-
// auth backend (`auth.aithos.be`) over plain HTTPS.
|
|
10
|
-
//
|
|
11
|
-
// Flow from the caller's point of view:
|
|
5
|
+
// One class, three flows, automatic session persistence. Apps shouldn't
|
|
6
|
+
// need to touch Argon2id, AES-GCM, or {@link sessionStorage} directly :
|
|
12
7
|
//
|
|
13
8
|
// const auth = new AithosAuth();
|
|
14
|
-
// // on a "Sign in" button:
|
|
15
|
-
// auth.signInWithGoogle({ appState: "/dashboard" }); // navigates away
|
|
16
9
|
//
|
|
17
|
-
// //
|
|
10
|
+
// // Sign in (existing account, email + password)
|
|
11
|
+
// const session = await auth.signIn({ email, password });
|
|
12
|
+
//
|
|
13
|
+
// // Sign up (creates an Aithos identity end-to-end)
|
|
14
|
+
// const { recoveryFile, ...session } = await auth.signUp({
|
|
15
|
+
// email, password, handle, displayName,
|
|
16
|
+
// });
|
|
17
|
+
//
|
|
18
|
+
// // Sign in with Google — redirects to Google's consent screen
|
|
19
|
+
// auth.signInWithGoogle({ appState: "/dashboard" });
|
|
20
|
+
//
|
|
21
|
+
// // Back on /auth/callback after Google
|
|
18
22
|
// const session = await auth.handleCallback();
|
|
19
|
-
// if (session) {
|
|
20
|
-
// // session.session — JWT, send as Bearer to /auth/blob etc.
|
|
21
|
-
// // session.enc_key_b64 — 32-byte vault key, base64 (raw bytes for
|
|
22
|
-
// // AES-GCM decryption of session.blob_b64)
|
|
23
|
-
// // session.is_first_login — true on the very first sign-in
|
|
24
|
-
// }
|
|
25
23
|
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
24
|
+
// // Anywhere — read the active session, null if signed out / expired
|
|
25
|
+
// const current = auth.getCurrentSession();
|
|
26
|
+
//
|
|
27
|
+
// // Sign out — clears the session store
|
|
28
|
+
// await auth.signOut();
|
|
29
|
+
//
|
|
30
|
+
// Storage : by default, sessions are persisted in `sessionStorage` (web)
|
|
31
|
+
// or no-op (Node / SSR). Apps with different needs pass their own
|
|
32
|
+
// `AithosSessionStore` to the constructor — see ./session-store.ts.
|
|
33
|
+
import { DEFAULT_KDF, buildBlobPlaintext, createBrowserIdentity, deriveAuthAndEncKeys, encryptBlob, randomNonce, randomSalt, serializeBlob, zeroize, } from "@aithos/protocol-client";
|
|
34
|
+
import { loginChallenge, loginVerify, registerAccount, } from "./auth-api.js";
|
|
35
|
+
import { defaultSessionStore, } from "./session-store.js";
|
|
30
36
|
import { AithosSDKError } from "./types.js";
|
|
31
37
|
/** Default URL of the Aithos auth backend. */
|
|
32
38
|
export const DEFAULT_AUTH_BASE_URL = "https://auth.aithos.be";
|
|
@@ -35,25 +41,185 @@ export const DEFAULT_AUTH_BASE_URL = "https://auth.aithos.be";
|
|
|
35
41
|
/* -------------------------------------------------------------------------- */
|
|
36
42
|
/**
|
|
37
43
|
* Authenticator for the Aithos identity service. One instance per app
|
|
38
|
-
* is the recommended pattern (the constructor is cheap
|
|
39
|
-
*
|
|
44
|
+
* is the recommended pattern (the constructor is cheap).
|
|
45
|
+
*
|
|
46
|
+
* The class is **stateful** in one specific way : it owns a session store
|
|
47
|
+
* that gets written on every successful auth call and read by
|
|
48
|
+
* {@link getCurrentSession}. Pass a custom store at construction time
|
|
49
|
+
* if you need different persistence (localStorage, IndexedDB, no-op).
|
|
40
50
|
*/
|
|
41
51
|
export class AithosAuth {
|
|
42
52
|
/** Resolved auth base URL with a trailing slash trimmed. */
|
|
43
53
|
authBaseUrl;
|
|
44
54
|
fetchImpl;
|
|
45
55
|
win;
|
|
56
|
+
store;
|
|
46
57
|
constructor(config = {}) {
|
|
47
58
|
this.authBaseUrl = trimSlash(config.authBaseUrl ?? DEFAULT_AUTH_BASE_URL);
|
|
48
59
|
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
49
60
|
this.win = config.window ?? (typeof window !== "undefined" ? window : undefined);
|
|
61
|
+
this.store = config.sessionStore ?? defaultSessionStore();
|
|
50
62
|
}
|
|
63
|
+
/* ------------------------------------------------------------------------ */
|
|
64
|
+
/* Email + password */
|
|
65
|
+
/* ------------------------------------------------------------------------ */
|
|
66
|
+
/**
|
|
67
|
+
* Sign in to an existing Aithos account. Two-round-trip flow under the
|
|
68
|
+
* hood :
|
|
69
|
+
*
|
|
70
|
+
* 1. POST /auth/login/challenge → server returns the salts + KDF params
|
|
71
|
+
* 2. derive auth_key from password + salt (Argon2id)
|
|
72
|
+
* 3. POST /auth/login/verify → server checks auth_key, returns JWT + blob
|
|
73
|
+
*
|
|
74
|
+
* The returned `enc_key_b64` is empty by design : password sign-in
|
|
75
|
+
* doesn't release the vault key over the wire (it's derived locally
|
|
76
|
+
* but discarded after the call). Apps that need the seeds — most
|
|
77
|
+
* apps don't — should use the (forthcoming) `loadEthos` helper
|
|
78
|
+
* separately with the password still in hand.
|
|
79
|
+
*
|
|
80
|
+
* Persists the session in the configured store before returning.
|
|
81
|
+
*/
|
|
82
|
+
async signIn(input) {
|
|
83
|
+
if (!input.email || !input.password) {
|
|
84
|
+
throw new AithosSDKError("auth_invalid_input", "signIn: email and password are required");
|
|
85
|
+
}
|
|
86
|
+
const challenge = await loginChallenge({ fetchImpl: this.fetchImpl, authBaseUrl: this.authBaseUrl }, input.email);
|
|
87
|
+
const { authKey, encKey } = await deriveAuthAndEncKeys(input.password, challenge.authSalt, challenge.encSalt, challenge.kdf);
|
|
88
|
+
// We don't keep enc_key around for the password flow — apps that
|
|
89
|
+
// need it can call deriveAuthAndEncKeys themselves with the password
|
|
90
|
+
// still available in their UI state. Wipe to be defensive.
|
|
91
|
+
zeroize(encKey);
|
|
92
|
+
let verify;
|
|
93
|
+
try {
|
|
94
|
+
verify = await loginVerify({ fetchImpl: this.fetchImpl, authBaseUrl: this.authBaseUrl }, input.email, authKey);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
zeroize(authKey);
|
|
98
|
+
}
|
|
99
|
+
const session = {
|
|
100
|
+
session: verify.session,
|
|
101
|
+
exp: verify.exp,
|
|
102
|
+
did: verify.did,
|
|
103
|
+
handle: verify.handle,
|
|
104
|
+
blob_b64: bytesToB64Public(verify.blob),
|
|
105
|
+
blob_nonce_b64: bytesToB64Public(verify.blobNonce),
|
|
106
|
+
blob_version: verify.blobVersion,
|
|
107
|
+
enc_key_b64: "",
|
|
108
|
+
is_first_login: false,
|
|
109
|
+
};
|
|
110
|
+
this.store.set(session);
|
|
111
|
+
return session;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Create a new Aithos account end-to-end :
|
|
115
|
+
*
|
|
116
|
+
* 1. Generate a fresh `BrowserIdentity` (4 Ed25519/X25519 seeds)
|
|
117
|
+
* 2. Build the recovery file (plaintext JSON, the user must save it)
|
|
118
|
+
* 3. Derive auth_key + enc_key from the password (Argon2id, fresh salts)
|
|
119
|
+
* 4. Encrypt the seeds in a vault blob (AES-GCM-256)
|
|
120
|
+
* 5. POST /auth/register with everything → JWT
|
|
121
|
+
* 6. Persist the session and return it + the recovery Blob
|
|
122
|
+
*
|
|
123
|
+
* The seeds are NOT published as an Aithos ethos here : the user's
|
|
124
|
+
* profile on `app.aithos.be` won't appear until they (or another app)
|
|
125
|
+
* publishes their first edition. This matches `aithos/app`'s design,
|
|
126
|
+
* where the vault is the source of truth for keys and the published
|
|
127
|
+
* edition is a separate concern.
|
|
128
|
+
*/
|
|
129
|
+
async signUp(input) {
|
|
130
|
+
if (!input.email || !input.password) {
|
|
131
|
+
throw new AithosSDKError("auth_invalid_input", "signUp: email and password are required");
|
|
132
|
+
}
|
|
133
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,62}$/i.test(input.handle)) {
|
|
134
|
+
throw new AithosSDKError("auth_invalid_handle", "signUp: handle must be 1–63 alphanumeric chars + _ -");
|
|
135
|
+
}
|
|
136
|
+
const displayName = input.displayName ?? input.handle;
|
|
137
|
+
// 1) Identity.
|
|
138
|
+
const identity = createBrowserIdentity(input.handle, displayName);
|
|
139
|
+
// 2) Recovery file — same shape as @aithos/protocol-client emits in
|
|
140
|
+
// `runOnboarding`, plaintext v0.1.0.
|
|
141
|
+
const recoveryJson = {
|
|
142
|
+
aithos_recovery_version: "0.1.0-plaintext",
|
|
143
|
+
handle: identity.handle,
|
|
144
|
+
display_name: identity.displayName,
|
|
145
|
+
did: identity.did,
|
|
146
|
+
seeds_hex: {
|
|
147
|
+
root: bytesToHex(identity.root.seed),
|
|
148
|
+
public: bytesToHex(identity.public.seed),
|
|
149
|
+
circle: bytesToHex(identity.circle.seed),
|
|
150
|
+
self: bytesToHex(identity.self.seed),
|
|
151
|
+
},
|
|
152
|
+
saved_at: new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
const recoveryFile = new Blob([JSON.stringify(recoveryJson, null, 2)], { type: "application/json" });
|
|
155
|
+
const recoveryFilename = `aithos-recovery-${identity.handle}.json`;
|
|
156
|
+
// 3) Derive password-based keys.
|
|
157
|
+
const authSalt = randomSalt();
|
|
158
|
+
const encSalt = randomSalt();
|
|
159
|
+
const kdf = DEFAULT_KDF;
|
|
160
|
+
const { authKey, encKey } = await deriveAuthAndEncKeys(input.password, authSalt, encSalt, kdf);
|
|
161
|
+
// 4) Build & encrypt the vault blob.
|
|
162
|
+
const plaintext = buildBlobPlaintext({
|
|
163
|
+
identity: {
|
|
164
|
+
did: identity.did,
|
|
165
|
+
handle: identity.handle,
|
|
166
|
+
displayName: identity.displayName,
|
|
167
|
+
},
|
|
168
|
+
seeds: {
|
|
169
|
+
root: identity.root.seed,
|
|
170
|
+
public: identity.public.seed,
|
|
171
|
+
circle: identity.circle.seed,
|
|
172
|
+
self: identity.self.seed,
|
|
173
|
+
},
|
|
174
|
+
delegates: [],
|
|
175
|
+
});
|
|
176
|
+
const blobBytes = serializeBlob(plaintext);
|
|
177
|
+
const blobNonce = randomNonce();
|
|
178
|
+
const blob = encryptBlob(encKey, blobNonce, blobBytes);
|
|
179
|
+
// 5) Register.
|
|
180
|
+
let registerResp;
|
|
181
|
+
try {
|
|
182
|
+
registerResp = await registerAccount({ fetchImpl: this.fetchImpl, authBaseUrl: this.authBaseUrl }, {
|
|
183
|
+
email: input.email,
|
|
184
|
+
handle: identity.handle,
|
|
185
|
+
displayName: identity.displayName,
|
|
186
|
+
did: identity.did,
|
|
187
|
+
authKey,
|
|
188
|
+
authSalt,
|
|
189
|
+
encSalt,
|
|
190
|
+
kdf,
|
|
191
|
+
blob,
|
|
192
|
+
blobNonce,
|
|
193
|
+
blobVersion: 1,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
zeroize(authKey);
|
|
198
|
+
zeroize(encKey);
|
|
199
|
+
}
|
|
200
|
+
const session = {
|
|
201
|
+
session: registerResp.session,
|
|
202
|
+
exp: registerResp.exp,
|
|
203
|
+
did: identity.did,
|
|
204
|
+
handle: identity.handle,
|
|
205
|
+
blob_b64: bytesToB64Public(blob),
|
|
206
|
+
blob_nonce_b64: bytesToB64Public(blobNonce),
|
|
207
|
+
blob_version: 1,
|
|
208
|
+
enc_key_b64: "",
|
|
209
|
+
is_first_login: false,
|
|
210
|
+
};
|
|
211
|
+
this.store.set(session);
|
|
212
|
+
return { session, recoveryFile, recoveryFilename };
|
|
213
|
+
}
|
|
214
|
+
/* ------------------------------------------------------------------------ */
|
|
215
|
+
/* Google SSO */
|
|
216
|
+
/* ------------------------------------------------------------------------ */
|
|
51
217
|
/**
|
|
52
218
|
* Redirect the browser to Google's OAuth consent screen. Must be called
|
|
53
219
|
* synchronously in response to a user gesture (button click) — most
|
|
54
220
|
* browsers block top-level navigation triggered from idle code.
|
|
55
221
|
*
|
|
56
|
-
* Does not return: navigation tears the JS context down. The `never`
|
|
222
|
+
* Does not return : navigation tears the JS context down. The `never`
|
|
57
223
|
* return type tells callers any code after the call is unreachable.
|
|
58
224
|
*/
|
|
59
225
|
signInWithGoogle(opts) {
|
|
@@ -68,21 +234,20 @@ export class AithosAuth {
|
|
|
68
234
|
url.searchParams.set("app_state", opts.appState);
|
|
69
235
|
}
|
|
70
236
|
this.win.location.assign(url.toString());
|
|
71
|
-
// Unreachable: location.assign navigates synchronously. The throw is
|
|
237
|
+
// Unreachable : location.assign navigates synchronously. The throw is
|
|
72
238
|
// belt-and-braces in case a caller awaits a microtask before unload.
|
|
73
239
|
throw new AithosSDKError("auth_redirecting", "redirecting to google");
|
|
74
240
|
}
|
|
75
241
|
/**
|
|
76
242
|
* Inspect the current URL for an `aithos_code` query parameter. If it's
|
|
77
|
-
* present, exchange it at the backend and return
|
|
78
|
-
*
|
|
243
|
+
* present, exchange it at the backend, persist the session, and return
|
|
244
|
+
* it. The query params are stripped from the URL via
|
|
79
245
|
* `history.replaceState` so a page refresh doesn't replay the redeem
|
|
80
246
|
* (which would 410 anyway).
|
|
81
247
|
*
|
|
82
248
|
* Returns `null` when there's no code in the URL — safe to call on every
|
|
83
249
|
* page load. Throws {@link AithosSDKError} on backend errors or when
|
|
84
|
-
* the URL carries `aithos_error
|
|
85
|
-
* failure, etc.).
|
|
250
|
+
* the URL carries `aithos_error=…`.
|
|
86
251
|
*/
|
|
87
252
|
async handleCallback() {
|
|
88
253
|
if (!this.win)
|
|
@@ -99,12 +264,16 @@ export class AithosAuth {
|
|
|
99
264
|
return null;
|
|
100
265
|
const session = await this.exchange(code);
|
|
101
266
|
cleanCallbackParams(this.win, here);
|
|
267
|
+
this.store.set(session);
|
|
102
268
|
return session;
|
|
103
269
|
}
|
|
104
270
|
/**
|
|
105
271
|
* Programmatically redeem an `aithos_code` for a session. `handleCallback`
|
|
106
|
-
* calls this for you; expose it directly for callers that already pulled
|
|
272
|
+
* calls this for you ; expose it directly for callers that already pulled
|
|
107
273
|
* the code out of the URL via their own router.
|
|
274
|
+
*
|
|
275
|
+
* Note : this method does NOT persist the session — it's the lower-level
|
|
276
|
+
* primitive. Use `handleCallback` for the full pipe.
|
|
108
277
|
*/
|
|
109
278
|
async exchange(aithosCode) {
|
|
110
279
|
const res = await this.fetchImpl(`${this.authBaseUrl}/auth/sso/exchange`, {
|
|
@@ -120,7 +289,9 @@ export class AithosAuth {
|
|
|
120
289
|
catch {
|
|
121
290
|
// ignore non-JSON error body
|
|
122
291
|
}
|
|
123
|
-
const code = typeof body?.["code"] === "string"
|
|
292
|
+
const code = typeof body?.["code"] === "string"
|
|
293
|
+
? `auth_${body["code"]}`
|
|
294
|
+
: "auth_exchange_failed";
|
|
124
295
|
const message = typeof body?.["error"] === "string"
|
|
125
296
|
? body["error"]
|
|
126
297
|
: `aithos_code redemption failed (${res.status})`;
|
|
@@ -131,14 +302,24 @@ export class AithosAuth {
|
|
|
131
302
|
}
|
|
132
303
|
return (await res.json());
|
|
133
304
|
}
|
|
305
|
+
/* ------------------------------------------------------------------------ */
|
|
306
|
+
/* Session lifecycle */
|
|
307
|
+
/* ------------------------------------------------------------------------ */
|
|
308
|
+
/**
|
|
309
|
+
* Read the active session from the configured store. Returns null if
|
|
310
|
+
* the user is signed out, or if the JWT has expired (the store
|
|
311
|
+
* auto-evicts expired entries — see ./session-store.ts).
|
|
312
|
+
*/
|
|
313
|
+
getCurrentSession() {
|
|
314
|
+
return this.store.get();
|
|
315
|
+
}
|
|
134
316
|
/**
|
|
135
|
-
* Stateless sign-out
|
|
136
|
-
* there's nothing to revoke server-side; this method
|
|
137
|
-
*
|
|
138
|
-
* own storage. The Promise always resolves.
|
|
317
|
+
* Stateless sign-out — the Aithos backend doesn't track sessions, so
|
|
318
|
+
* there's nothing to revoke server-side ; this method clears the
|
|
319
|
+
* configured session store and resolves.
|
|
139
320
|
*/
|
|
140
321
|
async signOut() {
|
|
141
|
-
|
|
322
|
+
this.store.clear();
|
|
142
323
|
}
|
|
143
324
|
}
|
|
144
325
|
/* -------------------------------------------------------------------------- */
|
|
@@ -153,4 +334,22 @@ function cleanCallbackParams(win, url) {
|
|
|
153
334
|
url.searchParams.delete("app_state");
|
|
154
335
|
win.history.replaceState(null, "", url.toString());
|
|
155
336
|
}
|
|
337
|
+
// Local copy of bytesToB64 — protocol-client exports it but we keep a
|
|
338
|
+
// thin local wrapper to avoid a name collision with public surface and
|
|
339
|
+
// to flag this is the "encode for the wire" path, not the "encode an
|
|
340
|
+
// auth_key" path.
|
|
341
|
+
function bytesToB64Public(bytes) {
|
|
342
|
+
if (bytes.length === 0)
|
|
343
|
+
return "";
|
|
344
|
+
let bin = "";
|
|
345
|
+
for (let i = 0; i < bytes.length; i++)
|
|
346
|
+
bin += String.fromCharCode(bytes[i]);
|
|
347
|
+
return btoa(bin).replace(/=+$/, "");
|
|
348
|
+
}
|
|
349
|
+
function bytesToHex(b) {
|
|
350
|
+
let out = "";
|
|
351
|
+
for (let i = 0; i < b.length; i++)
|
|
352
|
+
out += b[i].toString(16).padStart(2, "0");
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
156
355
|
//# sourceMappingURL=auth.js.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.0-alpha.
|
|
1
|
+
export declare const VERSION = "0.1.0-alpha.4";
|
|
2
2
|
export { AithosSDK } from "./sdk.js";
|
|
3
3
|
export type { AithosSDKConfig } from "./types.js";
|
|
4
4
|
export { AithosSDKError } from "./types.js";
|
|
@@ -9,7 +9,8 @@ export { ComputeNamespace } from "./compute.js";
|
|
|
9
9
|
export type { CreditPackId, CreateTopupSessionArgs, CreateTopupSessionResult, GetBalanceArgs, GetBalanceResult, } from "./wallet.js";
|
|
10
10
|
export { WalletNamespace } from "./wallet.js";
|
|
11
11
|
export { AithosAuth, DEFAULT_AUTH_BASE_URL } from "./auth.js";
|
|
12
|
-
export type { AithosAuthConfig, AithosSession, SignInWithGoogleOptions, } from "./auth.js";
|
|
12
|
+
export type { AithosAuthConfig, AithosSession, SignInInput, SignInWithGoogleOptions, SignUpInput, SignUpResult, } from "./auth.js";
|
|
13
|
+
export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, type AithosSessionStore, } from "./session-store.js";
|
|
13
14
|
export * as ethos from "./ethos.js";
|
|
14
15
|
export * as onboarding from "./onboarding.js";
|
|
15
16
|
export * as mandates from "./mandates.js";
|
package/dist/src/index.js
CHANGED
|
@@ -17,16 +17,22 @@
|
|
|
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.
|
|
20
|
+
export const VERSION = "0.1.0-alpha.4";
|
|
21
21
|
export { AithosSDK } from "./sdk.js";
|
|
22
22
|
export { AithosSDKError } from "./types.js";
|
|
23
23
|
export { DEFAULT_SDK_ENDPOINTS } from "./endpoints.js";
|
|
24
24
|
export { ComputeNamespace } from "./compute.js";
|
|
25
25
|
export { WalletNamespace } from "./wallet.js";
|
|
26
|
-
// Sign in with
|
|
27
|
-
//
|
|
28
|
-
// BrowserIdentity
|
|
26
|
+
// Sign-up, sign-in, sign-in-with-Google. Lives outside the AithosSDK
|
|
27
|
+
// class because the auth flow runs *before* the user has a
|
|
28
|
+
// BrowserIdentity (sign-up creates one, sign-in restores it from the
|
|
29
|
+
// server). The class also owns the session store — see
|
|
30
|
+
// ./session-store.ts for pluggable persistence.
|
|
29
31
|
export { AithosAuth, DEFAULT_AUTH_BASE_URL } from "./auth.js";
|
|
32
|
+
// Session storage backends used by AithosAuth. `sessionStorageStore` is
|
|
33
|
+
// the default in browser environments ; pass another store at construction
|
|
34
|
+
// time if you need different persistence.
|
|
35
|
+
export { DEFAULT_SESSION_STORAGE_KEY, defaultSessionStore, localStorageStore, noopStore, sessionStorageStore, } from "./session-store.js";
|
|
30
36
|
// Re-exports under stable namespace modules. Apps may also import these
|
|
31
37
|
// directly via `@aithos/protocol-client`; the SDK simply curates them.
|
|
32
38
|
export * as ethos from "./ethos.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AithosSession } from "./auth.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pluggable storage backend for the active session.
|
|
4
|
+
*
|
|
5
|
+
* Implementations should be **synchronous** : the SDK reads the store on
|
|
6
|
+
* boot to surface `getCurrentSession()` and async storage would force a
|
|
7
|
+
* Promise everywhere it's not warranted. Async backends should pre-warm
|
|
8
|
+
* the cache during the app's bootstrap and then implement the methods
|
|
9
|
+
* over that cache.
|
|
10
|
+
*
|
|
11
|
+
* `set` is called after every successful sign-in / sign-up / Google
|
|
12
|
+
* callback exchange. `clear` is called on `signOut`. `get` is called by
|
|
13
|
+
* `getCurrentSession()` and may also be called by the app on boot.
|
|
14
|
+
*/
|
|
15
|
+
export interface AithosSessionStore {
|
|
16
|
+
/** Read the persisted session. Return null if none, or if it's expired. */
|
|
17
|
+
get(): AithosSession | null;
|
|
18
|
+
/** Persist the session. Implementations should not throw — at worst
|
|
19
|
+
* they should warn and silently no-op (e.g. quota exceeded). */
|
|
20
|
+
set(session: AithosSession): void;
|
|
21
|
+
/** Wipe the persisted session. */
|
|
22
|
+
clear(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Storage key used by the bundled stores. Apps that want to coexist with
|
|
26
|
+
* other Aithos-aware libs (or that want to scope sessions per-tenant) can
|
|
27
|
+
* pass a custom key via {@link sessionStorageStore} or
|
|
28
|
+
* {@link localStorageStore}.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_SESSION_STORAGE_KEY = "aithos.session.v1";
|
|
31
|
+
interface WebStorageStoreOptions {
|
|
32
|
+
/** Storage key. Defaults to {@link DEFAULT_SESSION_STORAGE_KEY}. */
|
|
33
|
+
readonly key?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Default web store : `sessionStorage`. The session lives until the tab
|
|
37
|
+
* is closed. Cleared on `signOut()`. Use this when reauthenticating each
|
|
38
|
+
* day is acceptable and reduces blast radius after an XSS.
|
|
39
|
+
*/
|
|
40
|
+
export declare function sessionStorageStore(opts?: WebStorageStoreOptions): AithosSessionStore;
|
|
41
|
+
/**
|
|
42
|
+
* `localStorage` store. The session persists until the JWT expires or the
|
|
43
|
+
* user explicitly signs out. Higher convenience, larger XSS blast radius.
|
|
44
|
+
*/
|
|
45
|
+
export declare function localStorageStore(opts?: WebStorageStoreOptions): AithosSessionStore;
|
|
46
|
+
/**
|
|
47
|
+
* No-op store. `set` and `clear` discard their input ; `get` always
|
|
48
|
+
* returns null. The default in non-browser contexts (Node, edge runtimes)
|
|
49
|
+
* — apps running there should pass their own store explicitly.
|
|
50
|
+
*/
|
|
51
|
+
export declare function noopStore(): AithosSessionStore;
|
|
52
|
+
/**
|
|
53
|
+
* Pick a sensible default : `sessionStorage` if the browser environment
|
|
54
|
+
* is available, {@link noopStore} otherwise.
|
|
55
|
+
*/
|
|
56
|
+
export declare function defaultSessionStore(): AithosSessionStore;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=session-store.d.ts.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 Mathieu Colla
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* Storage key & expiration */
|
|
5
|
+
/* -------------------------------------------------------------------------- */
|
|
6
|
+
/**
|
|
7
|
+
* Storage key used by the bundled stores. Apps that want to coexist with
|
|
8
|
+
* other Aithos-aware libs (or that want to scope sessions per-tenant) can
|
|
9
|
+
* pass a custom key via {@link sessionStorageStore} or
|
|
10
|
+
* {@link localStorageStore}.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_SESSION_STORAGE_KEY = "aithos.session.v1";
|
|
13
|
+
/** Conservative buffer — drop the session 30 s before its `exp` so we
|
|
14
|
+
* don't hand out a token the server is about to reject. */
|
|
15
|
+
const SESSION_EXPIRY_BUFFER_S = 30;
|
|
16
|
+
function isExpired(session, nowSec) {
|
|
17
|
+
return session.exp <= nowSec + SESSION_EXPIRY_BUFFER_S;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate at runtime that an opaque object looks like an `AithosSession`.
|
|
21
|
+
* Storage values come from JSON.parse over user-controlled data — we can't
|
|
22
|
+
* trust them blind. This isn't a security check (the server validates the
|
|
23
|
+
* JWT ; persistence layers don't authenticate themselves) ; it just
|
|
24
|
+
* prevents weird crashes when the storage was tampered with.
|
|
25
|
+
*/
|
|
26
|
+
function isSessionShaped(v) {
|
|
27
|
+
if (typeof v !== "object" || v === null)
|
|
28
|
+
return false;
|
|
29
|
+
const o = v;
|
|
30
|
+
return (typeof o["session"] === "string" &&
|
|
31
|
+
typeof o["exp"] === "number" &&
|
|
32
|
+
typeof o["did"] === "string" &&
|
|
33
|
+
typeof o["handle"] === "string");
|
|
34
|
+
}
|
|
35
|
+
function browserStorageStore(storageRef, opts = {}) {
|
|
36
|
+
const key = opts.key ?? DEFAULT_SESSION_STORAGE_KEY;
|
|
37
|
+
return {
|
|
38
|
+
get() {
|
|
39
|
+
const s = storageRef();
|
|
40
|
+
if (!s)
|
|
41
|
+
return null;
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = s.getItem(key);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (!raw)
|
|
50
|
+
return null;
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Corrupted entry — wipe to recover.
|
|
57
|
+
try {
|
|
58
|
+
s.removeItem(key);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!isSessionShaped(parsed))
|
|
66
|
+
return null;
|
|
67
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
68
|
+
if (isExpired(parsed, nowSec)) {
|
|
69
|
+
// Auto-evict — let the caller see "no session" and re-auth.
|
|
70
|
+
try {
|
|
71
|
+
s.removeItem(key);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
},
|
|
80
|
+
set(session) {
|
|
81
|
+
const s = storageRef();
|
|
82
|
+
if (!s)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
s.setItem(key, JSON.stringify(session));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Quota exceeded, private mode, etc. — log but don't throw : the
|
|
89
|
+
// sign-in returned successfully, the in-memory session is still
|
|
90
|
+
// usable for this tab.
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.warn("[AithosAuth] failed to persist session:", e.message);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
clear() {
|
|
96
|
+
const s = storageRef();
|
|
97
|
+
if (!s)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
s.removeItem(key);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* ignore */
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function safeStorage(getter) {
|
|
109
|
+
return () => {
|
|
110
|
+
try {
|
|
111
|
+
const s = getter();
|
|
112
|
+
return s ?? null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Some restricted contexts (sandboxed iframes, file:// URLs) throw
|
|
116
|
+
// on access. Treat them as "no storage available".
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Default web store : `sessionStorage`. The session lives until the tab
|
|
123
|
+
* is closed. Cleared on `signOut()`. Use this when reauthenticating each
|
|
124
|
+
* day is acceptable and reduces blast radius after an XSS.
|
|
125
|
+
*/
|
|
126
|
+
export function sessionStorageStore(opts) {
|
|
127
|
+
return browserStorageStore(safeStorage(() => (typeof sessionStorage !== "undefined" ? sessionStorage : undefined)), opts);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `localStorage` store. The session persists until the JWT expires or the
|
|
131
|
+
* user explicitly signs out. Higher convenience, larger XSS blast radius.
|
|
132
|
+
*/
|
|
133
|
+
export function localStorageStore(opts) {
|
|
134
|
+
return browserStorageStore(safeStorage(() => (typeof localStorage !== "undefined" ? localStorage : undefined)), opts);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* No-op store. `set` and `clear` discard their input ; `get` always
|
|
138
|
+
* returns null. The default in non-browser contexts (Node, edge runtimes)
|
|
139
|
+
* — apps running there should pass their own store explicitly.
|
|
140
|
+
*/
|
|
141
|
+
export function noopStore() {
|
|
142
|
+
return {
|
|
143
|
+
get: () => null,
|
|
144
|
+
set: () => { },
|
|
145
|
+
clear: () => { },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Pick a sensible default : `sessionStorage` if the browser environment
|
|
150
|
+
* is available, {@link noopStore} otherwise.
|
|
151
|
+
*/
|
|
152
|
+
export function defaultSessionStore() {
|
|
153
|
+
if (typeof sessionStorage !== "undefined") {
|
|
154
|
+
return sessionStorageStore();
|
|
155
|
+
}
|
|
156
|
+
return noopStore();
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=session-store.js.map
|
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.4",
|
|
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",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"node": ">=20"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"@aithos/protocol-client": ">=0.1.0-alpha.
|
|
55
|
+
"@aithos/protocol-client": ">=0.1.0-alpha.11 <0.2.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@aithos/protocol-client": "^0.1.0-alpha.
|
|
58
|
+
"@aithos/protocol-client": "^0.1.0-alpha.11",
|
|
59
59
|
"@types/node": "^24.12.2",
|
|
60
60
|
"typescript": "^5.9.2"
|
|
61
61
|
},
|