@recursiv/sdk 0.7.50 → 0.7.52
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/README.md +69 -12
- package/dist/client.js +3 -1
- package/dist/index.d.ts +13 -6
- package/dist/index.js +5 -1
- package/dist/realtime.d.ts +79 -9
- package/dist/realtime.js +339 -76
- package/dist/resources/admin.d.ts +13 -0
- package/dist/resources/agents.d.ts +41 -4
- package/dist/resources/agents.js +45 -24
- package/dist/resources/app-subscriptions.d.ts +102 -4
- package/dist/resources/app-subscriptions.js +43 -3
- package/dist/resources/auth.d.ts +198 -2
- package/dist/resources/auth.js +397 -3
- package/dist/resources/boosts.d.ts +170 -0
- package/dist/resources/boosts.js +75 -0
- package/dist/resources/chat.d.ts +33 -3
- package/dist/resources/chat.js +38 -2
- package/dist/resources/communities.d.ts +23 -11
- package/dist/resources/communities.js +20 -6
- package/dist/resources/curator.d.ts +24 -6
- package/dist/resources/curator.js +2 -1
- package/dist/resources/deployments.d.ts +8 -3
- package/dist/resources/discovery.d.ts +15 -1
- package/dist/resources/discovery.js +29 -1
- package/dist/resources/invite-codes-admin.d.ts +13 -0
- package/dist/resources/invite-codes.d.ts +10 -0
- package/dist/resources/invite-codes.js +6 -6
- package/dist/resources/notifications.d.ts +74 -5
- package/dist/resources/notifications.js +38 -8
- package/dist/resources/posts.d.ts +28 -5
- package/dist/resources/posts.js +13 -4
- package/dist/resources/profiles.d.ts +45 -9
- package/dist/resources/profiles.js +54 -3
- package/dist/resources/protocols.d.ts +195 -14
- package/dist/resources/protocols.js +77 -12
- package/dist/resources/settings.d.ts +25 -3
- package/dist/resources/settings.js +28 -2
- package/dist/resources/video.d.ts +19 -0
- package/dist/resources/video.js +8 -0
- package/dist/resources/wallet.d.ts +41 -1
- package/dist/resources/wallet.js +8 -0
- package/dist/types.d.ts +217 -6
- package/llm.md +68 -20
- package/package.json +2 -2
package/dist/resources/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthenticationError, RecursivError, ValidationError } from '../errors.js';
|
|
1
|
+
import { AuthenticationError, RateLimitError, RecursivError, ValidationError } from '../errors.js';
|
|
2
2
|
/**
|
|
3
3
|
* Better Auth's anti-CSRF middleware requires an Origin header when the
|
|
4
4
|
* request carries cookies or otherwise looks browser-initiated. We only
|
|
@@ -21,6 +21,42 @@ const needsExplicitOrigin = !isBrowser && !isReactNative;
|
|
|
21
21
|
function originHeader(baseOrigin) {
|
|
22
22
|
return needsExplicitOrigin ? { Origin: baseOrigin } : {};
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The customer-app headers a keyless auth request carries so the server
|
|
26
|
+
* resolves the right network (user lookup) and brands emails for the app.
|
|
27
|
+
*/
|
|
28
|
+
function appContextHeaders(appContext) {
|
|
29
|
+
return {
|
|
30
|
+
...(appContext?.projectId ? { 'x-recursiv-app-project': appContext.projectId } : {}),
|
|
31
|
+
...(appContext?.organizationId ? { 'x-recursiv-app-organization': appContext.organizationId } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Better Auth answers a password sign-in for a two-factor account with
|
|
36
|
+
* `{ twoFactorRedirect: true }` and no session. Say so, instead of the
|
|
37
|
+
* generic "no session token" the caller would otherwise get, and hand over
|
|
38
|
+
* the challenge: the server returns it as `twoFactorToken` (Better Auth
|
|
39
|
+
* otherwise keeps it only in a cookie this SDK cannot carry). The error's
|
|
40
|
+
* `details` is a `TwoFactorChallenge`; pass its `twoFactorToken` and the code
|
|
41
|
+
* to `completeTwoFactorSignIn`.
|
|
42
|
+
*/
|
|
43
|
+
function throwIfTwoFactorRequired(data) {
|
|
44
|
+
const body = data;
|
|
45
|
+
if (body?.twoFactorRedirect === true) {
|
|
46
|
+
const details = {
|
|
47
|
+
twoFactorToken: typeof body.twoFactorToken === 'string' ? body.twoFactorToken : null,
|
|
48
|
+
methods: Array.isArray(body.twoFactorMethods)
|
|
49
|
+
? body.twoFactorMethods.filter((m) => typeof m === 'string')
|
|
50
|
+
: [],
|
|
51
|
+
};
|
|
52
|
+
throw new AuthenticationError({
|
|
53
|
+
type: 'auth_error',
|
|
54
|
+
message: 'This account has two-factor authentication on. Complete the second step to sign in.',
|
|
55
|
+
code: 'two_factor_required',
|
|
56
|
+
details,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
24
60
|
/**
|
|
25
61
|
* Extract the server's actual error code + message from an error body.
|
|
26
62
|
*
|
|
@@ -257,6 +293,69 @@ export class AuthResource {
|
|
|
257
293
|
}));
|
|
258
294
|
}
|
|
259
295
|
const data = await res.json();
|
|
296
|
+
throwIfTwoFactorRequired(data);
|
|
297
|
+
const token = this.extractToken(res, data);
|
|
298
|
+
if (!token) {
|
|
299
|
+
throw new RecursivError(0, {
|
|
300
|
+
type: 'auth_error',
|
|
301
|
+
message: 'No session token returned from sign in',
|
|
302
|
+
code: 'no_token',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
token,
|
|
307
|
+
user: {
|
|
308
|
+
id: data.user?.id,
|
|
309
|
+
name: data.user?.name,
|
|
310
|
+
email: data.user?.email,
|
|
311
|
+
image: data.user?.image ?? null,
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Sign in with a username (handle) and password.
|
|
317
|
+
*
|
|
318
|
+
* Handles are unique per network, not globally, so pass the customer app's
|
|
319
|
+
* `appContext` when the API is reached through a shared hostname: the server
|
|
320
|
+
* resolves the handle on that app's network and never on another. Returns a
|
|
321
|
+
* session token, like `signIn`. A two-factor account throws
|
|
322
|
+
* `two_factor_required`; finish with `completeTwoFactorSignIn`.
|
|
323
|
+
*/
|
|
324
|
+
async signInWithUsername(input, appContext) {
|
|
325
|
+
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/sign-in/username`, {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: {
|
|
328
|
+
'Content-Type': 'application/json',
|
|
329
|
+
...originHeader(this.baseOrigin),
|
|
330
|
+
...appContextHeaders(appContext),
|
|
331
|
+
},
|
|
332
|
+
body: JSON.stringify({ username: input.username.trim().replace(/^@/, ''), password: input.password }),
|
|
333
|
+
});
|
|
334
|
+
let data = null;
|
|
335
|
+
try {
|
|
336
|
+
data = await res.json();
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
data = null;
|
|
340
|
+
}
|
|
341
|
+
if (!res.ok) {
|
|
342
|
+
// 401 is a wrong handle or password; 422 is a handle that cannot exist.
|
|
343
|
+
// Both mean the same thing to the person typing, and neither may say
|
|
344
|
+
// which half was wrong.
|
|
345
|
+
if (res.status === 401 || res.status === 403 || res.status === 422) {
|
|
346
|
+
throw new AuthenticationError(serverError(data, {
|
|
347
|
+
type: 'auth_error',
|
|
348
|
+
message: 'Invalid username or password',
|
|
349
|
+
code: 'invalid_credentials',
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
throw new RecursivError(res.status, serverError(data, {
|
|
353
|
+
type: 'auth_error',
|
|
354
|
+
message: `Sign in failed: HTTP ${res.status}`,
|
|
355
|
+
code: 'signin_failed',
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
throwIfTwoFactorRequired(data);
|
|
260
359
|
const token = this.extractToken(res, data);
|
|
261
360
|
if (!token) {
|
|
262
361
|
throw new RecursivError(0, {
|
|
@@ -418,15 +517,20 @@ export class AuthResource {
|
|
|
418
517
|
* whether the email exists, to avoid leaking account existence.
|
|
419
518
|
*
|
|
420
519
|
* Sends the client's API key (when configured) so the server can brand the
|
|
421
|
-
* reset email for YOUR org — same surface-scoped rule as `sendOtp`.
|
|
520
|
+
* reset email for YOUR org — same surface-scoped rule as `sendOtp`. Keyless
|
|
521
|
+
* apps pass `appContext` (same shape as `sendOtp`) for the same effect.
|
|
422
522
|
*/
|
|
423
|
-
async forgetPassword(input) {
|
|
523
|
+
async forgetPassword(input, appContext) {
|
|
424
524
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/request-password-reset`, {
|
|
425
525
|
method: 'POST',
|
|
426
526
|
headers: {
|
|
427
527
|
'Content-Type': 'application/json',
|
|
428
528
|
...originHeader(this.baseOrigin),
|
|
429
529
|
...this.client.apiKeyAuthHeader(),
|
|
530
|
+
// Same as sendOtp: a keyless app names itself so the reset email is
|
|
531
|
+
// branded for it and the address is looked up on its network, not the
|
|
532
|
+
// platform default.
|
|
533
|
+
...appContextHeaders(appContext),
|
|
430
534
|
},
|
|
431
535
|
body: JSON.stringify({
|
|
432
536
|
email: input.email,
|
|
@@ -447,6 +551,50 @@ export class AuthResource {
|
|
|
447
551
|
}
|
|
448
552
|
return { success: true };
|
|
449
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Set a new password with the token from a password reset email (the
|
|
556
|
+
* `token` your reset screen receives). Every existing session and API key
|
|
557
|
+
* for the account is revoked by the server, so sign in again afterwards.
|
|
558
|
+
*/
|
|
559
|
+
async resetPassword(input, appContext) {
|
|
560
|
+
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/reset-password`, {
|
|
561
|
+
method: 'POST',
|
|
562
|
+
headers: {
|
|
563
|
+
'Content-Type': 'application/json',
|
|
564
|
+
...originHeader(this.baseOrigin),
|
|
565
|
+
...appContextHeaders(appContext),
|
|
566
|
+
},
|
|
567
|
+
body: JSON.stringify({ newPassword: input.newPassword, token: input.token }),
|
|
568
|
+
});
|
|
569
|
+
if (!res.ok) {
|
|
570
|
+
let body = {};
|
|
571
|
+
try {
|
|
572
|
+
body = await res.json();
|
|
573
|
+
}
|
|
574
|
+
catch { }
|
|
575
|
+
const info = serverErrorInfo(body);
|
|
576
|
+
if (res.status === 400 && (info.code === 'INVALID_TOKEN' || info.code === 'INVALID_TOKEN_OR_EXPIRED')) {
|
|
577
|
+
throw new ValidationError(serverError(body, {
|
|
578
|
+
type: 'validation_error',
|
|
579
|
+
message: 'This reset link is invalid or has expired. Request a new one.',
|
|
580
|
+
code: 'invalid_reset_token',
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
if (res.status === 400 || res.status === 422) {
|
|
584
|
+
throw new ValidationError(serverError(body, {
|
|
585
|
+
type: 'validation_error',
|
|
586
|
+
message: 'The new password was not accepted.',
|
|
587
|
+
code: 'invalid_password',
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
throw new RecursivError(res.status, serverError(body, {
|
|
591
|
+
type: 'auth_error',
|
|
592
|
+
message: `Password reset failed: HTTP ${res.status}`,
|
|
593
|
+
code: 'reset_password_failed',
|
|
594
|
+
}));
|
|
595
|
+
}
|
|
596
|
+
return { success: true };
|
|
597
|
+
}
|
|
450
598
|
/**
|
|
451
599
|
* Get current session info using a session token.
|
|
452
600
|
* Returns null if the session is invalid or expired.
|
|
@@ -577,6 +725,19 @@ export class AuthResource {
|
|
|
577
725
|
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
578
726
|
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
579
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* `signInWithUsername`, then the app API key, exactly like `signInAndCreateKey`.
|
|
730
|
+
* The key's project/organization is forwarded as the sign-in's app context,
|
|
731
|
+
* so the handle is resolved on the app's network.
|
|
732
|
+
*/
|
|
733
|
+
async signInWithUsernameAndCreateKey(credentials, keyInput) {
|
|
734
|
+
const session = await this.signInWithUsername(credentials, {
|
|
735
|
+
projectId: keyInput.projectId,
|
|
736
|
+
organizationId: keyInput.organizationId,
|
|
737
|
+
});
|
|
738
|
+
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
739
|
+
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
740
|
+
}
|
|
580
741
|
/**
|
|
581
742
|
* Complete mobile auth flow: sign up → create API key → return key.
|
|
582
743
|
*
|
|
@@ -597,6 +758,10 @@ export class AuthResource {
|
|
|
597
758
|
* Exchange a legacy Minds session (the OAuth access token or minds_sess JWT
|
|
598
759
|
* the legacy mobile app stored) for a session on this stack. Only answers
|
|
599
760
|
* during the cutover window; every failure means "show the OTP screen".
|
|
761
|
+
*
|
|
762
|
+
* A legacy token is single-use: after one successful exchange the server
|
|
763
|
+
* answers `legacy_token_used`, so erase the stored legacy token as soon as
|
|
764
|
+
* this resolves and never retry it.
|
|
600
765
|
*/
|
|
601
766
|
async legacyHandoff(legacyToken, appContext) {
|
|
602
767
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/legacy-handoff`, {
|
|
@@ -660,6 +825,235 @@ export class AuthResource {
|
|
|
660
825
|
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
661
826
|
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
662
827
|
}
|
|
828
|
+
// ── OAuth consent (MCP bridge) ──
|
|
829
|
+
/**
|
|
830
|
+
* Set the API-origin session cookie from a session token this client
|
|
831
|
+
* already holds, so a browser can continue an OAuth authorize request
|
|
832
|
+
* (`/api/auth/mcp/authorize`) without signing in again: the one-tap
|
|
833
|
+
* "Continue as @handle". Browser only in effect: the cookie is stored by the
|
|
834
|
+
* browser because the request is sent with `credentials: 'include'`.
|
|
835
|
+
* Returns the account the cookie now signs in.
|
|
836
|
+
*/
|
|
837
|
+
async exchangeSessionForCookie(sessionToken) {
|
|
838
|
+
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/session-cookie`, {
|
|
839
|
+
method: 'POST',
|
|
840
|
+
credentials: 'include',
|
|
841
|
+
headers: {
|
|
842
|
+
...originHeader(this.baseOrigin),
|
|
843
|
+
Authorization: `Bearer ${sessionToken}`,
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
let data = null;
|
|
847
|
+
try {
|
|
848
|
+
data = await res.json();
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
data = null;
|
|
852
|
+
}
|
|
853
|
+
if (!res.ok) {
|
|
854
|
+
const error = serverError(data, {
|
|
855
|
+
type: 'auth_error',
|
|
856
|
+
message: `Could not continue with this session: HTTP ${res.status}`,
|
|
857
|
+
code: 'session_cookie_failed',
|
|
858
|
+
});
|
|
859
|
+
if (res.status === 401 || res.status === 403)
|
|
860
|
+
throw new AuthenticationError(error);
|
|
861
|
+
throw new RecursivError(res.status, error);
|
|
862
|
+
}
|
|
863
|
+
return { user: data.user };
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Public facts about the OAuth client behind an authorize request: its name,
|
|
867
|
+
* icon, redirect hosts, whether `redirectUri` is one it registered, and which
|
|
868
|
+
* requested scopes are granted. No authentication needed.
|
|
869
|
+
*/
|
|
870
|
+
async getOAuthClientInfo(input) {
|
|
871
|
+
const query = new URLSearchParams({ client_id: input.clientId });
|
|
872
|
+
if (input.redirectUri !== undefined)
|
|
873
|
+
query.set('redirect_uri', input.redirectUri);
|
|
874
|
+
if (input.scope !== undefined)
|
|
875
|
+
query.set('scope', input.scope);
|
|
876
|
+
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/mcp/client-info?${query}`, { method: 'GET' });
|
|
877
|
+
let data = null;
|
|
878
|
+
try {
|
|
879
|
+
data = await res.json();
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
data = null;
|
|
883
|
+
}
|
|
884
|
+
if (!res.ok) {
|
|
885
|
+
throw new RecursivError(res.status, serverError(data, {
|
|
886
|
+
type: res.status === 404 ? 'not_found' : 'api_error',
|
|
887
|
+
message: res.status === 404 ? 'No OAuth client is registered with that client_id' : `Client lookup failed: HTTP ${res.status}`,
|
|
888
|
+
code: res.status === 404 ? 'client_not_found' : 'client_info_failed',
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
return data;
|
|
892
|
+
}
|
|
893
|
+
// ── Two-factor sign-in ──
|
|
894
|
+
/**
|
|
895
|
+
* Finish a password sign-in on a two-factor account. `signIn`,
|
|
896
|
+
* `signInWithUsername` and their `*AndCreateKey` forms throw
|
|
897
|
+
* `AuthenticationError` with code `two_factor_required`; its `details` is a
|
|
898
|
+
* `TwoFactorChallenge`. Send its `twoFactorToken` with the code the member
|
|
899
|
+
* types. Returns the session, like `signIn`.
|
|
900
|
+
*
|
|
901
|
+
* Errors keep the server's code: `INVALID_CODE` / `INVALID_BACKUP_CODE` (a
|
|
902
|
+
* wrong code: ask again), `INVALID_TWO_FACTOR_COOKIE` (the challenge expired
|
|
903
|
+
* or was used: sign in again), `TOO_MANY_ATTEMPTS_REQUEST_NEW_CODE` (five
|
|
904
|
+
* wrong codes on this challenge: sign in again), and
|
|
905
|
+
* `ACCOUNT_TEMPORARILY_LOCKED` (too many wrong codes on the account: wait
|
|
906
|
+
* `retryAfter` seconds).
|
|
907
|
+
*/
|
|
908
|
+
async completeTwoFactorSignIn(input) {
|
|
909
|
+
const path = input.method === 'backup_code' ? '/two-factor/verify-backup-code' : '/two-factor/verify-totp';
|
|
910
|
+
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth${path}`, {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
headers: {
|
|
913
|
+
'Content-Type': 'application/json',
|
|
914
|
+
...originHeader(this.baseOrigin),
|
|
915
|
+
},
|
|
916
|
+
body: JSON.stringify({ code: input.code.trim(), twoFactorToken: input.twoFactorToken }),
|
|
917
|
+
});
|
|
918
|
+
let data = null;
|
|
919
|
+
try {
|
|
920
|
+
data = await res.json();
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
data = null;
|
|
924
|
+
}
|
|
925
|
+
if (!res.ok) {
|
|
926
|
+
const retryAfter = Number(res.headers.get('retry-after'));
|
|
927
|
+
const error = serverError(data, {
|
|
928
|
+
type: 'auth_error',
|
|
929
|
+
message: res.status === 401 ? 'That code is not right' : `Two-factor sign-in failed: HTTP ${res.status}`,
|
|
930
|
+
code: res.status === 401 ? 'invalid_two_factor_code' : 'two_factor_sign_in_failed',
|
|
931
|
+
...(Number.isFinite(retryAfter) && retryAfter > 0 ? { retry_after: retryAfter } : {}),
|
|
932
|
+
});
|
|
933
|
+
if (res.status === 401)
|
|
934
|
+
throw new AuthenticationError(error);
|
|
935
|
+
if (res.status === 429)
|
|
936
|
+
throw new RateLimitError(error);
|
|
937
|
+
if (res.status === 400 || res.status === 422)
|
|
938
|
+
throw new ValidationError(error);
|
|
939
|
+
throw new RecursivError(res.status, error);
|
|
940
|
+
}
|
|
941
|
+
const token = this.extractToken(res, data);
|
|
942
|
+
if (!token) {
|
|
943
|
+
throw new RecursivError(0, {
|
|
944
|
+
type: 'auth_error',
|
|
945
|
+
message: 'No session token returned from two-factor sign in',
|
|
946
|
+
code: 'no_token',
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
token,
|
|
951
|
+
user: {
|
|
952
|
+
id: data.user?.id,
|
|
953
|
+
name: data.user?.name,
|
|
954
|
+
email: data.user?.email,
|
|
955
|
+
image: data.user?.image ?? null,
|
|
956
|
+
},
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* `completeTwoFactorSignIn`, then the app API key, exactly like
|
|
961
|
+
* `signInAndCreateKey`: the second half of a `*AndCreateKey` sign-in that
|
|
962
|
+
* threw `two_factor_required`.
|
|
963
|
+
*/
|
|
964
|
+
async completeTwoFactorSignInAndCreateKey(input, keyInput) {
|
|
965
|
+
const session = await this.completeTwoFactorSignIn(input);
|
|
966
|
+
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
967
|
+
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
968
|
+
}
|
|
969
|
+
// ── Two-factor (TOTP) ──
|
|
970
|
+
//
|
|
971
|
+
// These run against Better Auth with the member's SESSION token (the
|
|
972
|
+
// `session.token` from sign-in), not the API key. Verify (the call that
|
|
973
|
+
// turns two-factor on) and disable replace the session server-side, and the
|
|
974
|
+
// token passed in stops working. Every method that can rotate it returns
|
|
975
|
+
// the live `sessionToken`: store it.
|
|
976
|
+
async sessionRequest(path, body, sessionToken, fallback, appContext) {
|
|
977
|
+
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth${path}`, {
|
|
978
|
+
method: 'POST',
|
|
979
|
+
headers: {
|
|
980
|
+
'Content-Type': 'application/json',
|
|
981
|
+
...originHeader(this.baseOrigin),
|
|
982
|
+
...appContextHeaders(appContext),
|
|
983
|
+
Authorization: `Bearer ${sessionToken}`,
|
|
984
|
+
Cookie: `better-auth.session_token=${sessionToken}`,
|
|
985
|
+
},
|
|
986
|
+
body: JSON.stringify(body ?? {}),
|
|
987
|
+
});
|
|
988
|
+
let data = null;
|
|
989
|
+
try {
|
|
990
|
+
data = await res.json();
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
data = null;
|
|
994
|
+
}
|
|
995
|
+
if (!res.ok) {
|
|
996
|
+
const error = serverError(data, { type: 'auth_error', message: fallback.message, code: fallback.code });
|
|
997
|
+
if (res.status === 401)
|
|
998
|
+
throw new AuthenticationError({ ...error, code: error.code || 'session_required' });
|
|
999
|
+
if (res.status === 400 || res.status === 422)
|
|
1000
|
+
throw new ValidationError(error);
|
|
1001
|
+
throw new RecursivError(res.status, error);
|
|
1002
|
+
}
|
|
1003
|
+
return data;
|
|
1004
|
+
}
|
|
1005
|
+
static confirmationBody(input) {
|
|
1006
|
+
return {
|
|
1007
|
+
...(input.password ? { password: input.password } : {}),
|
|
1008
|
+
...(input.emailCode ? { emailCode: input.emailCode.trim() } : {}),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Email a two-factor confirmation code to an account that has no password.
|
|
1013
|
+
* An account with a password answers `PASSWORD_REQUIRED`: it confirms with
|
|
1014
|
+
* the password instead. Pass the app's `appContext` (as for `sendOtp`) when
|
|
1015
|
+
* the API is reached through a shared hostname, so the email carries the
|
|
1016
|
+
* app's brand rather than the platform's.
|
|
1017
|
+
*/
|
|
1018
|
+
async requestTwoFactorEmailCode(sessionToken, appContext) {
|
|
1019
|
+
const data = await this.sessionRequest('/two-factor/request-email-code', {}, sessionToken, { message: 'Could not send a confirmation code', code: 'two_factor_email_code_failed' }, appContext);
|
|
1020
|
+
return { expiresAt: data.expiresAt };
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Start two-factor enrollment. Returns the authenticator URI and backup
|
|
1024
|
+
* codes; two-factor is not on until `verifyTwoFactor` accepts a code from
|
|
1025
|
+
* the authenticator.
|
|
1026
|
+
*
|
|
1027
|
+
* An account with no password answers `PASSWORD_REQUIRED_FOR_TWO_FACTOR`
|
|
1028
|
+
* unless the server enables code-only enrollment: its sign-ins (email
|
|
1029
|
+
* code, passkey, social) do not ask for the second factor yet.
|
|
1030
|
+
*/
|
|
1031
|
+
async enableTwoFactor(input, sessionToken) {
|
|
1032
|
+
const data = await this.sessionRequest('/two-factor/enable', { ...AuthResource.confirmationBody(input), ...(input.issuer ? { issuer: input.issuer } : {}) }, sessionToken, { message: 'Could not start two-factor setup', code: 'two_factor_enable_failed' });
|
|
1033
|
+
return {
|
|
1034
|
+
totpURI: data.totpURI,
|
|
1035
|
+
backupCodes: Array.isArray(data.backupCodes) ? data.backupCodes : [],
|
|
1036
|
+
sessionToken: data?.session?.token || sessionToken,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Confirm enrollment with a code from the authenticator app. This is the
|
|
1041
|
+
* call that turns two-factor on.
|
|
1042
|
+
*/
|
|
1043
|
+
async verifyTwoFactor(input, sessionToken) {
|
|
1044
|
+
const data = await this.sessionRequest('/two-factor/verify-totp', { code: input.code.trim() }, sessionToken, { message: 'That code is not right', code: 'invalid_two_factor_code' });
|
|
1045
|
+
return { sessionToken: data?.session?.token || data?.token || sessionToken };
|
|
1046
|
+
}
|
|
1047
|
+
/** Turn two-factor off. Confirmed with the password, or an email code for accounts without one. */
|
|
1048
|
+
async disableTwoFactor(input, sessionToken) {
|
|
1049
|
+
const data = await this.sessionRequest('/two-factor/disable', AuthResource.confirmationBody(input), sessionToken, { message: 'Could not turn off two-factor', code: 'two_factor_disable_failed' });
|
|
1050
|
+
return { sessionToken: data?.session?.token || sessionToken };
|
|
1051
|
+
}
|
|
1052
|
+
/** Replace the backup codes. The old ones stop working. */
|
|
1053
|
+
async regenerateTwoFactorBackupCodes(input, sessionToken) {
|
|
1054
|
+
const data = await this.sessionRequest('/two-factor/generate-backup-codes', AuthResource.confirmationBody(input), sessionToken, { message: 'Could not create new backup codes', code: 'two_factor_backup_codes_failed' });
|
|
1055
|
+
return { backupCodes: Array.isArray(data?.backupCodes) ? data.backupCodes : [] };
|
|
1056
|
+
}
|
|
663
1057
|
/**
|
|
664
1058
|
* The key-mint leg of the `*AndCreateKey` flows. When the mint fails, the
|
|
665
1059
|
* caller receives only the thrown error — the session created one step
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { HttpClient } from '../client.js';
|
|
2
|
+
import type { PaginatedResponse, SingleResponse } from '../types.js';
|
|
3
|
+
/** Campaign lifecycle axis. */
|
|
4
|
+
export type BoostStatus = 'active' | 'paused' | 'completed' | 'refunded';
|
|
5
|
+
/** Campaign moderation axis. A campaign delivers only when `active` AND `approved`. */
|
|
6
|
+
export type BoostReviewStatus = 'pending' | 'approved' | 'rejected';
|
|
7
|
+
export type BoostRejectionReason = 'against_policy' | 'wrong_audience' | 'payment' | 'reported' | 'post_removed';
|
|
8
|
+
export type BoostLedgerKind = 'grant' | 'topup' | 'spend' | 'refund';
|
|
9
|
+
/** A promoted-post campaign. Money is micro-USD (1 USD = 1,000,000). */
|
|
10
|
+
export interface BoostCampaign {
|
|
11
|
+
id: string;
|
|
12
|
+
post_id: string;
|
|
13
|
+
booster_user_id: string;
|
|
14
|
+
/** Bid: micro-USD per 1000 impressions. */
|
|
15
|
+
cpm_micros: number;
|
|
16
|
+
budget_micros: number;
|
|
17
|
+
spent_micros: number;
|
|
18
|
+
impressions_target: number;
|
|
19
|
+
impressions_served: number;
|
|
20
|
+
status: BoostStatus;
|
|
21
|
+
review_status: BoostReviewStatus;
|
|
22
|
+
rejection_reason: BoostRejectionReason | null;
|
|
23
|
+
created_at: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BoostLedgerEntry {
|
|
27
|
+
id: string;
|
|
28
|
+
campaign_id: string | null;
|
|
29
|
+
/** Positive credits the balance, negative debits it. */
|
|
30
|
+
delta_micros: number;
|
|
31
|
+
kind: BoostLedgerKind;
|
|
32
|
+
note: string | null;
|
|
33
|
+
created_at: string;
|
|
34
|
+
}
|
|
35
|
+
export interface BoostTopupLimits {
|
|
36
|
+
/** Whether this app sells boost credit (always true on a `topup` response). */
|
|
37
|
+
available: boolean;
|
|
38
|
+
min_cents: number;
|
|
39
|
+
max_cents: number;
|
|
40
|
+
currency: 'usd';
|
|
41
|
+
}
|
|
42
|
+
export interface BoostBalance {
|
|
43
|
+
balance_micros: number;
|
|
44
|
+
/** The 50 most recent balance movements, newest first. */
|
|
45
|
+
ledger: BoostLedgerEntry[];
|
|
46
|
+
}
|
|
47
|
+
/** The boost credit the key's app includes with its paid plans. */
|
|
48
|
+
export interface BoostPlanCredit {
|
|
49
|
+
/** Micro-USD each paid billing period of a plan adds, by tier key (e.g. `plus`, `pro`). */
|
|
50
|
+
per_period_micros: Record<string, number>;
|
|
51
|
+
/** What your current plan on this app adds each paid period; 0 without one. */
|
|
52
|
+
current_micros: number;
|
|
53
|
+
/** When your next paid period starts and adds `current_micros`; null when none is due. */
|
|
54
|
+
next_grant_at: string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Plan credit you owe: taken back after your plan dropped or ended inside a period, but already
|
|
57
|
+
* spent. Your next plan credit repays it before adding anything. 0 when nothing is owed
|
|
58
|
+
* (absent from older servers).
|
|
59
|
+
*/
|
|
60
|
+
owed_micros?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface BoostBalanceResponse extends SingleResponse<BoostBalance> {
|
|
63
|
+
/** `plan_credit` is null when the app's plans include no boost credit (absent from older servers). */
|
|
64
|
+
meta: {
|
|
65
|
+
min_cpm_micros: number;
|
|
66
|
+
topup?: BoostTopupLimits;
|
|
67
|
+
plan_credit?: BoostPlanCredit | null;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export interface BoostCampaignResponse extends SingleResponse<BoostCampaign> {
|
|
71
|
+
meta?: {
|
|
72
|
+
min_cpm_micros?: number;
|
|
73
|
+
refunded_micros?: number;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface BoostEventResult {
|
|
77
|
+
recorded: boolean;
|
|
78
|
+
duplicate: boolean;
|
|
79
|
+
campaign_status: BoostStatus;
|
|
80
|
+
spent_micros: number;
|
|
81
|
+
impressions_served: number;
|
|
82
|
+
}
|
|
83
|
+
export interface BoostTopupCheckout {
|
|
84
|
+
/** Stripe Checkout URL to open. */
|
|
85
|
+
url: string;
|
|
86
|
+
/** Pass to `confirmTopup` when the member returns (the return URL also carries it as `session_id`). */
|
|
87
|
+
session_id: string;
|
|
88
|
+
}
|
|
89
|
+
export interface BoostTopupConfirmation {
|
|
90
|
+
/** `credited` now, `already_credited` earlier (webhook or a previous confirm), or `unpaid` so far. */
|
|
91
|
+
status: 'credited' | 'already_credited' | 'unpaid';
|
|
92
|
+
credited_micros: number;
|
|
93
|
+
balance_micros: number | null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Boost: pay to promote your own post into the For You feed. CPM campaigns
|
|
97
|
+
* funded from a prepaid balance with an append-only ledger. Top-ups run on the
|
|
98
|
+
* app's own Stripe account and need a project-scoped key.
|
|
99
|
+
*/
|
|
100
|
+
export declare class BoostsResource {
|
|
101
|
+
private client;
|
|
102
|
+
constructor(client: HttpClient);
|
|
103
|
+
/**
|
|
104
|
+
* Create a campaign for one of your own posts. Debits `budget_micros` from your balance
|
|
105
|
+
* (402 `insufficient_balance` when it cannot). Starts `pending` review.
|
|
106
|
+
* Not retried automatically, so a slow response cannot fund two campaigns.
|
|
107
|
+
*/
|
|
108
|
+
create(input: {
|
|
109
|
+
post_id: string;
|
|
110
|
+
cpm_micros: number;
|
|
111
|
+
budget_micros: number;
|
|
112
|
+
}): Promise<BoostCampaignResponse>;
|
|
113
|
+
/** Your campaigns, newest first, with spend and impressions. */
|
|
114
|
+
mine(params?: {
|
|
115
|
+
limit?: number;
|
|
116
|
+
offset?: number;
|
|
117
|
+
}): Promise<PaginatedResponse<BoostCampaign>>;
|
|
118
|
+
/**
|
|
119
|
+
* Your balance, recent ledger, the CPM floor, the top-up limits, and the boost credit your
|
|
120
|
+
* plan on this app adds each paid period (`meta.plan_credit`, a `grant` ledger row per period).
|
|
121
|
+
*/
|
|
122
|
+
balance(): Promise<BoostBalanceResponse>;
|
|
123
|
+
/** Pause one of your active campaigns. */
|
|
124
|
+
pause(campaignId: string): Promise<BoostCampaignResponse>;
|
|
125
|
+
/** Resume one of your paused campaigns. */
|
|
126
|
+
resume(campaignId: string): Promise<BoostCampaignResponse>;
|
|
127
|
+
/** Cancel a campaign and refund its unspent budget to your balance (`meta.refunded_micros`). */
|
|
128
|
+
cancel(campaignId: string): Promise<BoostCampaignResponse>;
|
|
129
|
+
/** Report an impression or click on a boosted feed entry. Idempotent per viewer per UTC day. */
|
|
130
|
+
recordEvent(campaignId: string, kind: 'impression' | 'click'): Promise<SingleResponse<BoostEventResult>>;
|
|
131
|
+
/**
|
|
132
|
+
* Buy boost credit: opens a one-time Stripe Checkout for `amount_cents` (USD) on the app's
|
|
133
|
+
* own Stripe account. The balance is credited once the payment succeeds. Limits are in
|
|
134
|
+
* `balance().meta.topup`. Requires a project-scoped key with `billing:write`.
|
|
135
|
+
* `return_url` keeps its own query and fragment; `topup=success&session_id=<id>` or
|
|
136
|
+
* `topup=cancelled` is added to its query (to the fragment's query for a hash route).
|
|
137
|
+
*/
|
|
138
|
+
topup(input: {
|
|
139
|
+
amount_cents: number;
|
|
140
|
+
return_url: string;
|
|
141
|
+
}): Promise<SingleResponse<BoostTopupCheckout> & {
|
|
142
|
+
meta: {
|
|
143
|
+
topup: BoostTopupLimits;
|
|
144
|
+
};
|
|
145
|
+
}>;
|
|
146
|
+
/**
|
|
147
|
+
* Credit a paid top-up when the member returns from Checkout. Safe to call repeatedly
|
|
148
|
+
* and alongside the webhook: a session credits the balance once.
|
|
149
|
+
*/
|
|
150
|
+
confirmTopup(sessionId: string): Promise<SingleResponse<BoostTopupConfirmation>>;
|
|
151
|
+
/** Campaigns waiting for review, oldest first (admin). */
|
|
152
|
+
listPendingReview(params?: {
|
|
153
|
+
limit?: number;
|
|
154
|
+
offset?: number;
|
|
155
|
+
}): Promise<PaginatedResponse<BoostCampaign>>;
|
|
156
|
+
/** Approve or reject a campaign (admin). Rejecting refunds the unspent budget. */
|
|
157
|
+
review(campaignId: string, input: {
|
|
158
|
+
action: 'approve' | 'reject';
|
|
159
|
+
reason?: BoostRejectionReason;
|
|
160
|
+
}): Promise<BoostCampaignResponse>;
|
|
161
|
+
/** Grant balance to a user (admin). */
|
|
162
|
+
grant(input: {
|
|
163
|
+
user_id: string;
|
|
164
|
+
amount_micros: number;
|
|
165
|
+
note?: string;
|
|
166
|
+
}): Promise<SingleResponse<{
|
|
167
|
+
user_id: string;
|
|
168
|
+
balance_micros: number;
|
|
169
|
+
}>>;
|
|
170
|
+
}
|