@aithos/sdk 0.1.0-alpha.44 → 0.1.0-alpha.47
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 +81 -0
- package/dist/src/auth-api.js +80 -0
- package/dist/src/auth.d.ts +84 -0
- package/dist/src/auth.js +133 -1
- package/dist/src/data.d.ts +184 -1
- package/dist/src/data.js +377 -29
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/internal/envelope.d.ts +16 -0
- package/dist/src/internal/envelope.js +6 -0
- package/dist/src/mandates.d.ts +35 -1
- package/dist/src/mandates.js +45 -3
- package/package.json +1 -1
package/dist/src/auth-api.d.ts
CHANGED
|
@@ -132,6 +132,87 @@ export type CustodialVerifyEmailApiResponse = {
|
|
|
132
132
|
* or past its TTL.
|
|
133
133
|
*/
|
|
134
134
|
export declare function custodialVerifyEmail(http: HttpClient, input: CustodialVerifyEmailApiInput): Promise<CustodialVerifyEmailApiResponse>;
|
|
135
|
+
/**
|
|
136
|
+
* Input for {@link custodialInvite}. The app authenticates via `apiKey`
|
|
137
|
+
* (server-only secret) or `publicKey` (browser-safe, Origin-gated). The
|
|
138
|
+
* `invitePayload` is an OPAQUE app string delivered verbatim to the invitee
|
|
139
|
+
* on {@link custodialAccept}. For Aithos invitations it's a delegate bundle
|
|
140
|
+
* (mandate + seed) serialized as JSON — but the auth backend never parses it;
|
|
141
|
+
* it stores it bound to a single-use token and returns it at accept time.
|
|
142
|
+
*/
|
|
143
|
+
export interface CustodialInviteApiInput {
|
|
144
|
+
readonly apiKey?: string;
|
|
145
|
+
readonly publicKey?: string;
|
|
146
|
+
readonly email: string;
|
|
147
|
+
/** Opaque payload delivered to the invitee on accept (e.g. a mandate bundle JSON). */
|
|
148
|
+
readonly invitePayload: string;
|
|
149
|
+
/** Token TTL in seconds. Backend clamps to its own min/max. Default backend-side. */
|
|
150
|
+
readonly ttlSeconds?: number;
|
|
151
|
+
/** Optional display name pre-filled on the pending account. */
|
|
152
|
+
readonly displayName?: string;
|
|
153
|
+
}
|
|
154
|
+
export interface CustodialInviteApiResponse {
|
|
155
|
+
readonly status: "invited";
|
|
156
|
+
readonly email: string;
|
|
157
|
+
readonly mailSent: boolean;
|
|
158
|
+
readonly mailMessageId?: string;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Send an invitation magic link carrying an opaque payload. The backend
|
|
162
|
+
* mints a single-use token, stores `{ token, email, invite_payload, ttl }`,
|
|
163
|
+
* and emails the magic link (same SES path as the verification mail). The
|
|
164
|
+
* payload (e.g. a mandate bundle) stays server-side — it never rides the
|
|
165
|
+
* email URL. The invitee redeems it via {@link custodialAccept}.
|
|
166
|
+
*
|
|
167
|
+
* No password here: this is an invitation, not an account creation. The
|
|
168
|
+
* invitee chooses their password when they accept.
|
|
169
|
+
*/
|
|
170
|
+
export declare function custodialInvite(http: HttpClient, input: CustodialInviteApiInput): Promise<CustodialInviteApiResponse>;
|
|
171
|
+
export interface CustodialAcceptApiInput {
|
|
172
|
+
readonly email: string;
|
|
173
|
+
readonly token: string;
|
|
174
|
+
/**
|
|
175
|
+
* Password. For a brand-new account the invitee SETS it here; for an
|
|
176
|
+
* existing account they AUTHENTICATE with it. Required unless the backend
|
|
177
|
+
* accepts a session-bound redemption (not in v1).
|
|
178
|
+
*/
|
|
179
|
+
readonly password?: string;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Result of redeeming an invitation. Always `signed_in` on success — the
|
|
183
|
+
* token is consumed, the account is created (or the existing one is
|
|
184
|
+
* authenticated), and a full session payload is returned alongside the
|
|
185
|
+
* `invitePayload` the inviter attached. `accountCreated` distinguishes the
|
|
186
|
+
* two cases for UX.
|
|
187
|
+
*/
|
|
188
|
+
export interface CustodialAcceptApiResponse {
|
|
189
|
+
readonly status: "signed_in";
|
|
190
|
+
readonly session: string;
|
|
191
|
+
readonly exp: number;
|
|
192
|
+
readonly did: string;
|
|
193
|
+
readonly handle: string;
|
|
194
|
+
readonly displayName: string;
|
|
195
|
+
readonly seed: Uint8Array;
|
|
196
|
+
readonly encKey: Uint8Array;
|
|
197
|
+
readonly blob: Uint8Array;
|
|
198
|
+
readonly blobNonce: Uint8Array;
|
|
199
|
+
readonly blobVersion: number;
|
|
200
|
+
/** The opaque payload the inviter attached (e.g. a mandate bundle JSON). */
|
|
201
|
+
readonly invitePayload: string;
|
|
202
|
+
/** True when a new account was provisioned; false when an existing one signed in. */
|
|
203
|
+
readonly accountCreated: boolean;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Redeem an invitation token from the magic link. Consumes the token
|
|
207
|
+
* (single-use), creates the account with the supplied password (or
|
|
208
|
+
* authenticates an existing account), and returns a session + the inviter's
|
|
209
|
+
* `invitePayload`.
|
|
210
|
+
*
|
|
211
|
+
* Throws `auth_token_invalid_or_expired` (bad/consumed/expired token) or an
|
|
212
|
+
* auth error if an existing account's password is wrong / a new password is
|
|
213
|
+
* too weak.
|
|
214
|
+
*/
|
|
215
|
+
export declare function custodialAccept(http: HttpClient, input: CustodialAcceptApiInput): Promise<CustodialAcceptApiResponse>;
|
|
135
216
|
/** Re-send the verification mail for a pending account. The backend
|
|
136
217
|
* is anti-enum (always 200) and rate-limited 1/h/account, so this is
|
|
137
218
|
* safe to call even when the user state is unknown. Accepts the same
|
package/dist/src/auth-api.js
CHANGED
|
@@ -183,6 +183,86 @@ export async function custodialVerifyEmail(http, input) {
|
|
|
183
183
|
blobVersion: wire.blob_version,
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Send an invitation magic link carrying an opaque payload. The backend
|
|
188
|
+
* mints a single-use token, stores `{ token, email, invite_payload, ttl }`,
|
|
189
|
+
* and emails the magic link (same SES path as the verification mail). The
|
|
190
|
+
* payload (e.g. a mandate bundle) stays server-side — it never rides the
|
|
191
|
+
* email URL. The invitee redeems it via {@link custodialAccept}.
|
|
192
|
+
*
|
|
193
|
+
* No password here: this is an invitation, not an account creation. The
|
|
194
|
+
* invitee chooses their password when they accept.
|
|
195
|
+
*/
|
|
196
|
+
export async function custodialInvite(http, input) {
|
|
197
|
+
if (!input.apiKey && !input.publicKey) {
|
|
198
|
+
throw new AithosSDKError("auth_missing_api_key", "inviteCustodial requires either apiKey or publicKey");
|
|
199
|
+
}
|
|
200
|
+
if (input.apiKey && input.publicKey) {
|
|
201
|
+
throw new AithosSDKError("auth_invalid_input", "inviteCustodial: pass exactly one of apiKey or publicKey, not both");
|
|
202
|
+
}
|
|
203
|
+
const bearer = (input.apiKey ?? input.publicKey);
|
|
204
|
+
const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/invite`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"content-type": "application/json",
|
|
208
|
+
authorization: `Bearer ${bearer}`,
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
email: input.email,
|
|
212
|
+
invite_payload: input.invitePayload,
|
|
213
|
+
...(input.ttlSeconds !== undefined ? { ttl_seconds: input.ttlSeconds } : {}),
|
|
214
|
+
...(input.displayName ? { display_name: input.displayName } : {}),
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok)
|
|
218
|
+
throw await readError(res, "custodial_invite_failed");
|
|
219
|
+
const wire = (await res.json());
|
|
220
|
+
return {
|
|
221
|
+
status: "invited",
|
|
222
|
+
email: wire.email,
|
|
223
|
+
mailSent: wire.mail_sent,
|
|
224
|
+
...(wire.mail_message_id !== undefined ? { mailMessageId: wire.mail_message_id } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Redeem an invitation token from the magic link. Consumes the token
|
|
229
|
+
* (single-use), creates the account with the supplied password (or
|
|
230
|
+
* authenticates an existing account), and returns a session + the inviter's
|
|
231
|
+
* `invitePayload`.
|
|
232
|
+
*
|
|
233
|
+
* Throws `auth_token_invalid_or_expired` (bad/consumed/expired token) or an
|
|
234
|
+
* auth error if an existing account's password is wrong / a new password is
|
|
235
|
+
* too weak.
|
|
236
|
+
*/
|
|
237
|
+
export async function custodialAccept(http, input) {
|
|
238
|
+
const res = await http.fetchImpl(`${http.authBaseUrl}/auth/custodial/accept`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "content-type": "application/json" },
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
email: input.email,
|
|
243
|
+
token: input.token,
|
|
244
|
+
...(input.password ? { password: input.password } : {}),
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok)
|
|
248
|
+
throw await readError(res, "custodial_accept_failed");
|
|
249
|
+
const wire = (await res.json());
|
|
250
|
+
return {
|
|
251
|
+
status: "signed_in",
|
|
252
|
+
session: wire.session,
|
|
253
|
+
exp: wire.exp,
|
|
254
|
+
did: wire.did,
|
|
255
|
+
handle: wire.handle,
|
|
256
|
+
displayName: wire.display_name,
|
|
257
|
+
seed: b64ToBytes(wire.seed_b64),
|
|
258
|
+
encKey: b64ToBytes(wire.enc_key_b64),
|
|
259
|
+
blob: wire.blob_b64 ? b64ToBytes(wire.blob_b64) : new Uint8Array(0),
|
|
260
|
+
blobNonce: wire.blob_nonce_b64 ? b64ToBytes(wire.blob_nonce_b64) : new Uint8Array(0),
|
|
261
|
+
blobVersion: wire.blob_version,
|
|
262
|
+
invitePayload: wire.invite_payload,
|
|
263
|
+
accountCreated: wire.account_created,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
186
266
|
/* ---- POST /auth/custodial/verify/resend -------------------------------- */
|
|
187
267
|
/** Re-send the verification mail for a pending account. The backend
|
|
188
268
|
* is anti-enum (always 200) and rate-limited 1/h/account, so this is
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -152,6 +152,64 @@ export interface ImportMandateInput {
|
|
|
152
152
|
/** Delegate bundle as a Blob or already-decoded JSON string. */
|
|
153
153
|
readonly bundle: Blob | string;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Input to {@link AithosAuth.inviteCustodial}. Sends an invitation magic link
|
|
157
|
+
* that carries an opaque payload (typically a delegate bundle) to deliver to
|
|
158
|
+
* the invitee on accept. Mandate-agnostic: `mandateBundle` may be ANY mandate
|
|
159
|
+
* (read/write/append/ethos/compute…) — the auth backend stores it verbatim,
|
|
160
|
+
* bound to a single-use token, and never parses it.
|
|
161
|
+
*
|
|
162
|
+
* The app authenticates via `apiKey` (server-only secret) or `publicKey`
|
|
163
|
+
* (browser-safe, Origin-gated), or the constructor's default `publicKey`.
|
|
164
|
+
* No user password here — the invitee chooses it when they accept.
|
|
165
|
+
*/
|
|
166
|
+
export interface InviteCustodialInput {
|
|
167
|
+
readonly apiKey?: string;
|
|
168
|
+
readonly publicKey?: string;
|
|
169
|
+
/** Invitee email — receives the magic link. */
|
|
170
|
+
readonly email: string;
|
|
171
|
+
/**
|
|
172
|
+
* The mandate to deliver. Accept the SDK's `MintedMandate.bundle` (Blob),
|
|
173
|
+
* a JSON string, or a plain bundle object — all normalized to a JSON string.
|
|
174
|
+
*/
|
|
175
|
+
readonly mandateBundle: Blob | string | Record<string, unknown>;
|
|
176
|
+
/** Token TTL in seconds (backend clamps to its policy). */
|
|
177
|
+
readonly ttlSeconds?: number;
|
|
178
|
+
/** Optional display name pre-filled on the pending account. */
|
|
179
|
+
readonly displayName?: string;
|
|
180
|
+
}
|
|
181
|
+
/** Result of {@link AithosAuth.inviteCustodial}. */
|
|
182
|
+
export interface InviteCustodialResult {
|
|
183
|
+
readonly status: "invited";
|
|
184
|
+
readonly email: string;
|
|
185
|
+
readonly mailSent: boolean;
|
|
186
|
+
readonly mailMessageId?: string;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Input to {@link AithosAuth.acceptInvite}. `email` and `token` come from the
|
|
190
|
+
* `?email=&token=` query string of the invitation link. `password` is set by
|
|
191
|
+
* the invitee for a NEW account, or used to authenticate an EXISTING one — so
|
|
192
|
+
* it's required whenever the user isn't already signed in.
|
|
193
|
+
*/
|
|
194
|
+
export interface AcceptInviteInput {
|
|
195
|
+
readonly email: string;
|
|
196
|
+
readonly token: string;
|
|
197
|
+
readonly password?: string;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Result of {@link AithosAuth.acceptInvite}. The token is consumed, the
|
|
201
|
+
* session is hydrated (account created or existing one signed in), and the
|
|
202
|
+
* invited mandate has been imported into the keystore — `delegate` describes
|
|
203
|
+
* it. Read `delegate.subjectDid` to identify the issuer (e.g. resolve their
|
|
204
|
+
* public Ethos via `sdk.ethos.of(delegate.subjectDid)`).
|
|
205
|
+
*/
|
|
206
|
+
export interface AcceptInviteResult {
|
|
207
|
+
readonly status: "signed_in";
|
|
208
|
+
readonly session: AithosSession;
|
|
209
|
+
readonly delegate: DelegateInfo;
|
|
210
|
+
/** True when a new account was provisioned; false when an existing one signed in. */
|
|
211
|
+
readonly accountCreated: boolean;
|
|
212
|
+
}
|
|
155
213
|
/**
|
|
156
214
|
* Input to {@link AithosAuth.signUpCustodial}.
|
|
157
215
|
*
|
|
@@ -544,6 +602,32 @@ export declare class AithosAuth {
|
|
|
544
602
|
* past its 1h TTL — surface a "request a fresh link" CTA in that case.
|
|
545
603
|
*/
|
|
546
604
|
verifyEmail(input: VerifyEmailInput): Promise<VerifyEmailResult>;
|
|
605
|
+
/**
|
|
606
|
+
* Send an invitation magic link carrying a mandate. The issuer (owner)
|
|
607
|
+
* mints any mandate via {@link AithosSDK.mandates} (read/write/append/…),
|
|
608
|
+
* then calls this with the bundle: the auth backend stores it bound to a
|
|
609
|
+
* single-use token and emails the magic link. The mandate (and its delegate
|
|
610
|
+
* seed) never ride the email URL. The invitee redeems it via
|
|
611
|
+
* {@link acceptInvite}.
|
|
612
|
+
*
|
|
613
|
+
* Generic — knows nothing about the mandate's scope. Authenticate with
|
|
614
|
+
* `apiKey` (server) or `publicKey` (browser, Origin-gated).
|
|
615
|
+
*/
|
|
616
|
+
inviteCustodial(input: InviteCustodialInput): Promise<InviteCustodialResult>;
|
|
617
|
+
/**
|
|
618
|
+
* Redeem an invitation from the magic link: consume the token, sign in
|
|
619
|
+
* (create the account with `password`, or authenticate an existing one),
|
|
620
|
+
* and AUTO-IMPORT the mandate the inviter attached. Returns the session and
|
|
621
|
+
* the imported {@link DelegateInfo}.
|
|
622
|
+
*
|
|
623
|
+
* Mount this on the page declared as the invitation's verify/redirect URL;
|
|
624
|
+
* read `email` + `token` from `window.location.search`, collect the
|
|
625
|
+
* `password`, call this.
|
|
626
|
+
*
|
|
627
|
+
* Throws `auth_token_invalid_or_expired` (bad/consumed/expired token) or an
|
|
628
|
+
* auth error if an existing account's password is wrong / a new one is weak.
|
|
629
|
+
*/
|
|
630
|
+
acceptInvite(input: AcceptInviteInput): Promise<AcceptInviteResult>;
|
|
547
631
|
/**
|
|
548
632
|
* Re-send the verification mail for a pending account. Use when the
|
|
549
633
|
* user reports never having received the welcome mail, or when their
|
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 { browserIdentityFromStored, buildBlobPlaintext, buildSignedEnvelope, createBrowserIdentity, decryptBlob, DEFAULT_KDF, deriveAuthAndEncKeys, encryptBlob, parseBlob, randomNonce, randomSalt, serializeBlob, signedDidDocument, zeroize, } from "@aithos/protocol-client";
|
|
24
|
-
import { custodialResendVerify, custodialResetFinalize, custodialResetRequest, custodialSignIn, custodialSignUp, custodialVerifyEmail, loginChallenge, loginVerify, putBlob, registerAccount, } from "./auth-api.js";
|
|
24
|
+
import { custodialAccept, custodialInvite, custodialResendVerify, custodialResetFinalize, custodialResetRequest, custodialSignIn, custodialSignUp, custodialVerifyEmail, loginChallenge, loginVerify, putBlob, registerAccount, } from "./auth-api.js";
|
|
25
25
|
import { defaultSessionStore, } from "./session-store.js";
|
|
26
26
|
import { defaultKeyStore, } from "./key-store.js";
|
|
27
27
|
import { parseDelegateBundle, readDelegateBundleText, } from "./internal/delegate-bundle.js";
|
|
@@ -1029,6 +1029,138 @@ export class AithosAuth {
|
|
|
1029
1029
|
this.#sessionStore.set(session);
|
|
1030
1030
|
return { status: "signed_in", session, passwordMustChange: false };
|
|
1031
1031
|
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Send an invitation magic link carrying a mandate. The issuer (owner)
|
|
1034
|
+
* mints any mandate via {@link AithosSDK.mandates} (read/write/append/…),
|
|
1035
|
+
* then calls this with the bundle: the auth backend stores it bound to a
|
|
1036
|
+
* single-use token and emails the magic link. The mandate (and its delegate
|
|
1037
|
+
* seed) never ride the email URL. The invitee redeems it via
|
|
1038
|
+
* {@link acceptInvite}.
|
|
1039
|
+
*
|
|
1040
|
+
* Generic — knows nothing about the mandate's scope. Authenticate with
|
|
1041
|
+
* `apiKey` (server) or `publicKey` (browser, Origin-gated).
|
|
1042
|
+
*/
|
|
1043
|
+
async inviteCustodial(input) {
|
|
1044
|
+
if (!input.email) {
|
|
1045
|
+
throw new AithosSDKError("auth_invalid_input", "inviteCustodial: email is required");
|
|
1046
|
+
}
|
|
1047
|
+
if (input.mandateBundle === undefined || input.mandateBundle === null) {
|
|
1048
|
+
throw new AithosSDKError("auth_invalid_input", "inviteCustodial: mandateBundle is required");
|
|
1049
|
+
}
|
|
1050
|
+
const apiKey = input.apiKey;
|
|
1051
|
+
const publicKey = input.publicKey ?? this.#publicKey;
|
|
1052
|
+
if (!apiKey && !publicKey) {
|
|
1053
|
+
throw new AithosSDKError("auth_missing_api_key", "inviteCustodial: pass apiKey, or publicKey, or set publicKey on the AithosAuth constructor");
|
|
1054
|
+
}
|
|
1055
|
+
// Normalize the bundle to a JSON string (the opaque invite payload).
|
|
1056
|
+
let invitePayload;
|
|
1057
|
+
if (typeof input.mandateBundle === "string") {
|
|
1058
|
+
invitePayload = input.mandateBundle;
|
|
1059
|
+
}
|
|
1060
|
+
else if (input.mandateBundle instanceof Blob) {
|
|
1061
|
+
invitePayload = await input.mandateBundle.text();
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
invitePayload = JSON.stringify(input.mandateBundle);
|
|
1065
|
+
}
|
|
1066
|
+
const result = await custodialInvite({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, {
|
|
1067
|
+
email: input.email,
|
|
1068
|
+
invitePayload,
|
|
1069
|
+
...(apiKey ? { apiKey } : publicKey ? { publicKey } : {}),
|
|
1070
|
+
...(input.ttlSeconds !== undefined ? { ttlSeconds: input.ttlSeconds } : {}),
|
|
1071
|
+
...(input.displayName ? { displayName: input.displayName } : {}),
|
|
1072
|
+
});
|
|
1073
|
+
return {
|
|
1074
|
+
status: "invited",
|
|
1075
|
+
email: result.email,
|
|
1076
|
+
mailSent: result.mailSent,
|
|
1077
|
+
...(result.mailMessageId !== undefined ? { mailMessageId: result.mailMessageId } : {}),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Redeem an invitation from the magic link: consume the token, sign in
|
|
1082
|
+
* (create the account with `password`, or authenticate an existing one),
|
|
1083
|
+
* and AUTO-IMPORT the mandate the inviter attached. Returns the session and
|
|
1084
|
+
* the imported {@link DelegateInfo}.
|
|
1085
|
+
*
|
|
1086
|
+
* Mount this on the page declared as the invitation's verify/redirect URL;
|
|
1087
|
+
* read `email` + `token` from `window.location.search`, collect the
|
|
1088
|
+
* `password`, call this.
|
|
1089
|
+
*
|
|
1090
|
+
* Throws `auth_token_invalid_or_expired` (bad/consumed/expired token) or an
|
|
1091
|
+
* auth error if an existing account's password is wrong / a new one is weak.
|
|
1092
|
+
*/
|
|
1093
|
+
async acceptInvite(input) {
|
|
1094
|
+
if (!input.email || !input.token) {
|
|
1095
|
+
throw new AithosSDKError("auth_invalid_input", "acceptInvite: email and token are required");
|
|
1096
|
+
}
|
|
1097
|
+
const resp = await custodialAccept({ fetchImpl: this.#fetchImpl, authBaseUrl: this.authBaseUrl }, {
|
|
1098
|
+
email: input.email,
|
|
1099
|
+
token: input.token,
|
|
1100
|
+
...(input.password ? { password: input.password } : {}),
|
|
1101
|
+
});
|
|
1102
|
+
// Materialise the 4 sphere seeds + session — same shape as the verifyEmail
|
|
1103
|
+
// magic-link path. Kept inline (additive; verifyEmail is left untouched).
|
|
1104
|
+
if (resp.seed.byteLength !== 128) {
|
|
1105
|
+
zeroize(resp.seed);
|
|
1106
|
+
zeroize(resp.encKey);
|
|
1107
|
+
throw new AithosSDKError("auth_custodial_seed_format", `acceptInvite: expected 128-byte seed bundle, got ${resp.seed.byteLength}`);
|
|
1108
|
+
}
|
|
1109
|
+
const seedRoot = resp.seed.slice(0, 32);
|
|
1110
|
+
const seedPublic = resp.seed.slice(32, 64);
|
|
1111
|
+
const seedCircle = resp.seed.slice(64, 96);
|
|
1112
|
+
const seedSelf = resp.seed.slice(96, 128);
|
|
1113
|
+
const stored = {
|
|
1114
|
+
version: "0.1.0-hex",
|
|
1115
|
+
did: resp.did,
|
|
1116
|
+
handle: resp.handle,
|
|
1117
|
+
displayName: resp.displayName,
|
|
1118
|
+
seedsHex: {
|
|
1119
|
+
root: bytesToHex(seedRoot),
|
|
1120
|
+
public: bytesToHex(seedPublic),
|
|
1121
|
+
circle: bytesToHex(seedCircle),
|
|
1122
|
+
self: bytesToHex(seedSelf),
|
|
1123
|
+
},
|
|
1124
|
+
savedAt: new Date().toISOString(),
|
|
1125
|
+
};
|
|
1126
|
+
zeroize(resp.seed);
|
|
1127
|
+
zeroize(seedRoot);
|
|
1128
|
+
zeroize(seedPublic);
|
|
1129
|
+
zeroize(seedCircle);
|
|
1130
|
+
zeroize(seedSelf);
|
|
1131
|
+
zeroize(resp.encKey);
|
|
1132
|
+
const identity = browserIdentityFromStored({
|
|
1133
|
+
handle: stored.handle,
|
|
1134
|
+
displayName: stored.displayName,
|
|
1135
|
+
did: stored.did,
|
|
1136
|
+
seeds: stored.seedsHex,
|
|
1137
|
+
});
|
|
1138
|
+
await this.#publishIdentity(identity);
|
|
1139
|
+
if (this.#ownerSigners)
|
|
1140
|
+
this.#ownerSigners.destroy();
|
|
1141
|
+
this.#ownerSigners = OwnerSigners.fromStoredOwnerKeys(stored);
|
|
1142
|
+
await this.#keyStore.saveOwner(stored);
|
|
1143
|
+
const session = {
|
|
1144
|
+
session: resp.session,
|
|
1145
|
+
exp: resp.exp,
|
|
1146
|
+
did: resp.did,
|
|
1147
|
+
handle: resp.handle,
|
|
1148
|
+
blob_b64: bytesToB64Public(resp.blob),
|
|
1149
|
+
blob_nonce_b64: bytesToB64Public(resp.blobNonce),
|
|
1150
|
+
blob_version: resp.blobVersion,
|
|
1151
|
+
enc_key_b64: "",
|
|
1152
|
+
is_first_login: false,
|
|
1153
|
+
};
|
|
1154
|
+
this.#sessionStore.set(session);
|
|
1155
|
+
// Import the invited mandate into the keystore (generic — any scope).
|
|
1156
|
+
const delegate = await this.importMandate({ bundle: resp.invitePayload });
|
|
1157
|
+
return {
|
|
1158
|
+
status: "signed_in",
|
|
1159
|
+
session,
|
|
1160
|
+
delegate,
|
|
1161
|
+
accountCreated: resp.accountCreated,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1032
1164
|
/**
|
|
1033
1165
|
* Re-send the verification mail for a pending account. Use when the
|
|
1034
1166
|
* user reports never having received the welcome mail, or when their
|
package/dist/src/data.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SignedMandate } from "@aithos/protocol-client";
|
|
1
2
|
export interface AithosSchemaLite {
|
|
2
3
|
readonly schema: string;
|
|
3
4
|
readonly indexable: ReadonlySet<string>;
|
|
@@ -45,12 +46,26 @@ export interface CreateDataClientArgs {
|
|
|
45
46
|
export interface DataClient {
|
|
46
47
|
/** Get / create a collection handle. */
|
|
47
48
|
collection(name: string): DataCollection;
|
|
48
|
-
/** Initialize a new collection with an explicit schema.
|
|
49
|
+
/** Initialize a new collection with an explicit schema. Throws
|
|
50
|
+
* `-32073 AITHOS_DATA_COLLECTION_EXISTS` if it already exists. */
|
|
49
51
|
createCollection(args: {
|
|
50
52
|
name: string;
|
|
51
53
|
schema: string;
|
|
52
54
|
forwardSecrecy?: "best_effort" | "strict";
|
|
53
55
|
}): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Get-or-create: create the collection if it doesn't exist, otherwise
|
|
58
|
+
* succeed silently. Idempotent — safe to call on every app boot before
|
|
59
|
+
* writing. Absorbs the `-32073 AITHOS_DATA_COLLECTION_EXISTS` conflict
|
|
60
|
+
* (and the concurrent-create race) so callers don't have to special-case
|
|
61
|
+
* "already there". Avoids the friction where `collection(name).insert(…)`
|
|
62
|
+
* on a never-created collection fails with `-32020`.
|
|
63
|
+
*/
|
|
64
|
+
ensureCollection(args: {
|
|
65
|
+
name: string;
|
|
66
|
+
schema: string;
|
|
67
|
+
forwardSecrecy?: "best_effort" | "strict";
|
|
68
|
+
}): Promise<void>;
|
|
54
69
|
/** List collections owned by this subject. */
|
|
55
70
|
listCollections(): Promise<readonly {
|
|
56
71
|
name: string;
|
|
@@ -102,9 +117,83 @@ export interface DataClient {
|
|
|
102
117
|
getSchema(schemaId: string, opts?: {
|
|
103
118
|
subjectDid?: string;
|
|
104
119
|
}): Promise<object | null>;
|
|
120
|
+
/**
|
|
121
|
+
* Grant a mandate-holding delegate read access to one of this owner's
|
|
122
|
+
* collections, by re-wrapping the collection's CMK to the grantee's
|
|
123
|
+
* key and posting `aithos.data.authorize_app`.
|
|
124
|
+
*
|
|
125
|
+
* Owner-only. The CMK is unwrapped locally (the owner holds it), then
|
|
126
|
+
* re-wrapped X25519-HKDF-AEAD to the grantee's X25519 key (derived
|
|
127
|
+
* from `mandate.grantee.pubkey`). The platform never sees the CMK in
|
|
128
|
+
* clear — it only appends the wrap to the collection's envelope after
|
|
129
|
+
* verifying the mandate (data spec §4.5).
|
|
130
|
+
*
|
|
131
|
+
* Idempotent at the server: re-authorizing the same grantee on the
|
|
132
|
+
* same collection is a no-op. One wrap per grantee covers every record
|
|
133
|
+
* in the collection (O(1) authorization — the CMK is stable).
|
|
134
|
+
*
|
|
135
|
+
* The mandate must carry a `data.<collectionName>.{read|write|admin}`
|
|
136
|
+
* or `data.*.*` scope and a `grantee.pubkey`.
|
|
137
|
+
*/
|
|
138
|
+
authorizeDelegate(args: {
|
|
139
|
+
collectionName: string;
|
|
140
|
+
mandate: SignedMandate;
|
|
141
|
+
}): Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Revoke a delegate's access to a collection (`aithos.data.revoke_app`).
|
|
144
|
+
* Owner-only, forward-only: after revocation the PDS refuses the
|
|
145
|
+
* delegate's reads (the mandate is marked revoked), and the delegate's
|
|
146
|
+
* wrap is dropped from the collection's authorization index. Already-read
|
|
147
|
+
* / cached plaintext on the delegate side is out of scope (a known limit
|
|
148
|
+
* of any key-sharing scheme — revocation blocks FUTURE access).
|
|
149
|
+
*/
|
|
150
|
+
revokeDelegate(args: {
|
|
151
|
+
collectionName: string;
|
|
152
|
+
mandateId: string;
|
|
153
|
+
reason?: string;
|
|
154
|
+
}): Promise<void>;
|
|
105
155
|
/** Drop in-memory cache (CMK, collection metadata, …). */
|
|
106
156
|
reset(): void;
|
|
107
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Read-only view over a subject's data collections, driven by a mandate
|
|
160
|
+
* the subject granted to a delegate (`data.<collection>.read`). Built by
|
|
161
|
+
* {@link createDelegateDataClient}.
|
|
162
|
+
*
|
|
163
|
+
* Mirror of {@link DataClient} minus every mutating verb: a delegate
|
|
164
|
+
* holding a read mandate can `get`/`list` and enumerate collections, but
|
|
165
|
+
* cannot insert, update, delete, create collections, register schemas, or
|
|
166
|
+
* re-delegate. Those throw `-32042` client-side (and the PDS rejects them
|
|
167
|
+
* server-side regardless).
|
|
168
|
+
*/
|
|
169
|
+
export interface ReadonlyDataClient {
|
|
170
|
+
/** Get a read-only collection handle. */
|
|
171
|
+
collection(name: string): ReadonlyDataCollection;
|
|
172
|
+
/** List collections the delegate's mandate scopes can reach. */
|
|
173
|
+
listCollections(): Promise<readonly {
|
|
174
|
+
name: string;
|
|
175
|
+
schema: string;
|
|
176
|
+
record_count: number;
|
|
177
|
+
}[]>;
|
|
178
|
+
/** List gamma audit entries (read). */
|
|
179
|
+
listGammaEntries(opts?: {
|
|
180
|
+
limit?: number;
|
|
181
|
+
opPrefix?: string;
|
|
182
|
+
verify?: boolean;
|
|
183
|
+
}): Promise<unknown>;
|
|
184
|
+
/** Drop in-memory cache (CMK, collection metadata, …). */
|
|
185
|
+
reset(): void;
|
|
186
|
+
}
|
|
187
|
+
export interface ReadonlyDataCollection {
|
|
188
|
+
readonly name: string;
|
|
189
|
+
/** Fetch one record by id (decrypted client-side via the re-wrapped CMK). */
|
|
190
|
+
get(recordId: string): Promise<Record<string, unknown> | null>;
|
|
191
|
+
/** List records, decrypted. Pagination via opaque cursor. */
|
|
192
|
+
list(opts?: ListOpts): Promise<{
|
|
193
|
+
items: Record<string, unknown>[];
|
|
194
|
+
nextCursor?: string;
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
108
197
|
export interface DataCollection {
|
|
109
198
|
readonly name: string;
|
|
110
199
|
/**
|
|
@@ -150,4 +239,98 @@ export interface ListOpts {
|
|
|
150
239
|
readonly cursor?: string;
|
|
151
240
|
}
|
|
152
241
|
export declare function createDataClient(args: CreateDataClientArgs): DataClient;
|
|
242
|
+
export interface CreateDelegateDataClientArgs {
|
|
243
|
+
/** PDS base URL (same endpoint the owner writes to). */
|
|
244
|
+
readonly pdsUrl: string;
|
|
245
|
+
/** DID of the SUBJECT whose data is being read (the mandate issuer). */
|
|
246
|
+
readonly subjectDid: string;
|
|
247
|
+
/**
|
|
248
|
+
* The full signed mandate the subject granted to this delegate. Must
|
|
249
|
+
* carry a `data.<collection>.read` (or wider) scope and a
|
|
250
|
+
* `grantee.pubkey` matching `delegateSeed`.
|
|
251
|
+
*/
|
|
252
|
+
readonly mandate: SignedMandate;
|
|
253
|
+
/** The delegate's Ed25519 seed (32 bytes) — the grantee key the mandate
|
|
254
|
+
* is bound to. Used to sign envelopes AND to derive the X25519 key that
|
|
255
|
+
* unwraps the re-wrapped CMK. */
|
|
256
|
+
readonly delegateSeed: Uint8Array;
|
|
257
|
+
/**
|
|
258
|
+
* The delegate's Ed25519 public key, multibase-encoded. Defaults to
|
|
259
|
+
* `mandate.grantee.pubkey`. This is the bare verificationMethod the PDS
|
|
260
|
+
* binds the delegate envelope to.
|
|
261
|
+
*/
|
|
262
|
+
readonly granteePubkeyMultibase?: string;
|
|
263
|
+
/** App-defined (vendor) schemas, as for {@link createDataClient}. */
|
|
264
|
+
readonly schemas?: readonly AithosSchemaLite[];
|
|
265
|
+
/** `fetch` override (tests). */
|
|
266
|
+
readonly fetch?: typeof fetch;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build a read-only data client that reads a subject's collections under
|
|
270
|
+
* a mandate (delegate path). The returned {@link ReadonlyDataClient}
|
|
271
|
+
* signs every request as the delegate (bare-multibase verificationMethod
|
|
272
|
+
* + the mandate attached to the envelope), and decrypts records using the
|
|
273
|
+
* CMK the owner re-wrapped for this delegate via
|
|
274
|
+
* {@link DataClient.authorizeDelegate}.
|
|
275
|
+
*
|
|
276
|
+
* Writes are not available on the returned type and throw `-32042` if
|
|
277
|
+
* forced.
|
|
278
|
+
*/
|
|
279
|
+
export declare function createDelegateDataClient(args: CreateDelegateDataClientArgs): ReadonlyDataClient;
|
|
280
|
+
/** An append-only handle on one collection: `insert` and nothing else. */
|
|
281
|
+
export interface AppendOnlyDataCollection {
|
|
282
|
+
readonly name: string;
|
|
283
|
+
/**
|
|
284
|
+
* Deposit a record. The DEK is sealed to the owner's public key, so the
|
|
285
|
+
* depositor cannot read this (or any) record back. Returns the record id.
|
|
286
|
+
*/
|
|
287
|
+
insert(record: Record<string, unknown>): Promise<string>;
|
|
288
|
+
}
|
|
289
|
+
/** A client holding a `data.<collection>.append` mandate. Insert-only. */
|
|
290
|
+
export interface AppendOnlyDataClient {
|
|
291
|
+
/** Get an append-only handle on a collection (schema supplied at
|
|
292
|
+
* construction — append clients cannot read collection metadata). */
|
|
293
|
+
collection(name: string): AppendOnlyDataCollection;
|
|
294
|
+
/** Drop in-memory cache. */
|
|
295
|
+
reset(): void;
|
|
296
|
+
}
|
|
297
|
+
export interface CreateAppendDataClientArgs {
|
|
298
|
+
/** PDS base URL (same endpoint the owner writes to). */
|
|
299
|
+
readonly pdsUrl: string;
|
|
300
|
+
/** DID of the SUBJECT who owns the target collection (the mandate issuer). */
|
|
301
|
+
readonly subjectDid: string;
|
|
302
|
+
/**
|
|
303
|
+
* The owner's `#data` Ed25519 public key (multibase z…). The depositor
|
|
304
|
+
* derives the owner's X25519 wrap target from it and seals each DEK to it.
|
|
305
|
+
* Source: the append mandate / invitation, or the owner's DID document.
|
|
306
|
+
*/
|
|
307
|
+
readonly ownerDataPubkeyMultibase: string;
|
|
308
|
+
/** The signed mandate carrying `data.<collection>.append`. */
|
|
309
|
+
readonly mandate: SignedMandate;
|
|
310
|
+
/** The depositor's Ed25519 seed (32 bytes) — the grantee key the mandate is
|
|
311
|
+
* bound to. Signs each insert envelope. NEVER used to read. */
|
|
312
|
+
readonly delegateSeed: Uint8Array;
|
|
313
|
+
/** Defaults to `mandate.grantee.pubkey`. */
|
|
314
|
+
readonly granteePubkeyMultibase?: string;
|
|
315
|
+
/**
|
|
316
|
+
* Schema of the target collection(s). The append client builds records
|
|
317
|
+
* locally (it cannot fetch collection metadata), so the caller MUST supply
|
|
318
|
+
* the schema(s) used by the collection it deposits into. The first entry is
|
|
319
|
+
* used as the default; multiple may be passed for multi-collection clients.
|
|
320
|
+
*/
|
|
321
|
+
readonly schema: AithosSchemaLite;
|
|
322
|
+
/** Additional schemas (looked up by id alongside `schema`). */
|
|
323
|
+
readonly schemas?: readonly AithosSchemaLite[];
|
|
324
|
+
/** `fetch` override (tests). */
|
|
325
|
+
readonly fetch?: typeof fetch;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Build an **append-only** data client from a `data.<collection>.append`
|
|
329
|
+
* mandate. The returned {@link AppendOnlyDataClient} can ONLY `insert`: it
|
|
330
|
+
* seals each record's DEK to the owner's public key (never the CMK), so it
|
|
331
|
+
* holds no read capability — it cannot decrypt anything in the collection,
|
|
332
|
+
* not even its own deposit. The PDS additionally enforces the append scope
|
|
333
|
+
* (insert allowed; get/list/update/delete refused).
|
|
334
|
+
*/
|
|
335
|
+
export declare function createAppendDataClient(args: CreateAppendDataClientArgs): AppendOnlyDataClient;
|
|
153
336
|
//# sourceMappingURL=data.d.ts.map
|