@recursiv/sdk 0.7.54 → 0.7.57
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 +28 -19
- package/dist/client.d.ts +1 -0
- package/dist/client.js +10 -2
- package/dist/errors.d.ts +41 -1
- package/dist/errors.js +77 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/realtime.d.ts +24 -1
- package/dist/realtime.js +29 -1
- package/dist/resources/auth.d.ts +46 -10
- package/dist/resources/auth.js +99 -38
- package/dist/resources/commands.d.ts +14 -1
- package/dist/resources/commands.js +7 -1
- package/dist/resources/e2ee.d.ts +221 -6
- package/dist/resources/e2ee.js +90 -0
- package/dist/resources/human-input.d.ts +4 -1
- package/dist/resources/human-input.js +7 -3
- package/dist/resources/moderation.d.ts +5 -1
- package/dist/resources/moderation.js +6 -2
- package/dist/resources/notifications.d.ts +19 -0
- package/dist/resources/protocols.d.ts +52 -5
- package/dist/resources/protocols.js +13 -4
- package/dist/resources/reports.d.ts +4 -0
- package/dist/resources/reports.js +5 -1
- package/dist/types.d.ts +6 -0
- package/llm.md +26 -6
- package/package.json +1 -1
package/dist/resources/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthenticationError, RateLimitError, RecursivError, ValidationError } from '../errors.js';
|
|
1
|
+
import { AuthenticationError, LegacyHandoffError, 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
|
|
@@ -89,6 +89,14 @@ function serverErrorInfo(body) {
|
|
|
89
89
|
: undefined,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
/** Seconds from a Retry-After header given in seconds; null when absent or not a number. */
|
|
93
|
+
function retryAfterHeaderSeconds(res) {
|
|
94
|
+
const raw = res.headers.get('retry-after');
|
|
95
|
+
if (!raw)
|
|
96
|
+
return null;
|
|
97
|
+
const seconds = Number(raw);
|
|
98
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
99
|
+
}
|
|
92
100
|
/** Normalize the flat, nested-object, and legacy nested-string auth errors. */
|
|
93
101
|
function serverError(body, fallback) {
|
|
94
102
|
const record = (body ?? {});
|
|
@@ -634,10 +642,16 @@ export class AuthResource {
|
|
|
634
642
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/sign-out`, {
|
|
635
643
|
method: 'POST',
|
|
636
644
|
headers: {
|
|
645
|
+
// Better Auth refuses a POST without a JSON Content-Type (415
|
|
646
|
+
// UNSUPPORTED_MEDIA_TYPE) before its handler runs, and on the Node
|
|
647
|
+
// server every POST carries a body stream, even an empty one. Without
|
|
648
|
+
// these two lines no sign-out ever reached the session.
|
|
649
|
+
'Content-Type': 'application/json',
|
|
637
650
|
...originHeader(this.baseOrigin),
|
|
638
651
|
Authorization: `Bearer ${sessionToken}`,
|
|
639
652
|
Cookie: `better-auth.session_token=${sessionToken}`,
|
|
640
653
|
},
|
|
654
|
+
body: '{}',
|
|
641
655
|
});
|
|
642
656
|
if (!res.ok) {
|
|
643
657
|
let body = {};
|
|
@@ -762,53 +776,96 @@ export class AuthResource {
|
|
|
762
776
|
/**
|
|
763
777
|
* Exchange a legacy Minds session (the OAuth access token or minds_sess JWT
|
|
764
778
|
* the legacy mobile app stored) for a session on this stack. Only answers
|
|
765
|
-
* during the cutover window
|
|
779
|
+
* during the cutover window.
|
|
766
780
|
*
|
|
767
|
-
* A
|
|
768
|
-
*
|
|
769
|
-
*
|
|
781
|
+
* A failure throws `LegacyHandoffError` with the HTTP status (0 when no
|
|
782
|
+
* response arrived), the server's code and its Retry-After. Only
|
|
783
|
+
* `legacy_token_used` and `legacy_token_invalid` mean the legacy session can
|
|
784
|
+
* never be exchanged; forget it then. Anything else (`legacy_unavailable`,
|
|
785
|
+
* `legacy_handoff_in_progress`, a rate limit, a closed window, an account
|
|
786
|
+
* not moved yet, no response at all) leaves it worth trying again: keep it.
|
|
787
|
+
*
|
|
788
|
+
* `attemptId` is a random id (22 to 128 URL-safe characters) the caller
|
|
789
|
+
* keeps with the legacy session and sends with every exchange of it. The
|
|
790
|
+
* server binds its single-use claim to it, so a retry after a lost answer is
|
|
791
|
+
* answered again instead of refused. `refreshToken` is the legacy OAuth
|
|
792
|
+
* refresh token; the server redeems it only when the access token cannot
|
|
793
|
+
* prove the session by itself. `timeoutMs` bounds this call, body included
|
|
794
|
+
* (default: the client's timeout). With an `attemptId`, the answer carries
|
|
795
|
+
* `apiKeyDescription`: the description to give the API key made with it.
|
|
770
796
|
*/
|
|
771
|
-
async legacyHandoff(legacyToken,
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
},
|
|
782
|
-
body: JSON.stringify({ legacy_token: legacyToken }),
|
|
783
|
-
});
|
|
784
|
-
let data = null;
|
|
797
|
+
async legacyHandoff(legacyToken, options = {}) {
|
|
798
|
+
const body = { legacy_token: legacyToken };
|
|
799
|
+
if (options.attemptId)
|
|
800
|
+
body.attempt_id = options.attemptId;
|
|
801
|
+
if (options.refreshToken)
|
|
802
|
+
body.legacy_refresh_token = options.refreshToken;
|
|
803
|
+
const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0 ? options.timeoutMs : this.timeout;
|
|
804
|
+
const fail = (status, code, message, retryAfter = null) => new LegacyHandoffError(status, { type: 'auth_error', code, message, ...(retryAfter !== null ? { retry_after: retryAfter } : {}) });
|
|
805
|
+
const controller = new AbortController();
|
|
806
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
785
807
|
try {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
808
|
+
let res;
|
|
809
|
+
try {
|
|
810
|
+
res = await fetch(`${this.baseOrigin}/api/auth/legacy-handoff`, {
|
|
811
|
+
method: 'POST',
|
|
812
|
+
headers: {
|
|
813
|
+
'Content-Type': 'application/json',
|
|
814
|
+
...originHeader(this.baseOrigin),
|
|
815
|
+
...(options.projectId ? { 'x-recursiv-app-project': options.projectId } : {}),
|
|
816
|
+
...(options.organizationId ? { 'x-recursiv-app-organization': options.organizationId } : {}),
|
|
817
|
+
},
|
|
818
|
+
body: JSON.stringify(body),
|
|
819
|
+
signal: controller.signal,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
if (controller.signal.aborted)
|
|
824
|
+
throw fail(0, 'timeout', `Legacy handoff timed out after ${timeoutMs}ms`);
|
|
825
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
826
|
+
throw fail(0, 'network_error', `Legacy handoff failed before a response arrived (${detail})`);
|
|
827
|
+
}
|
|
828
|
+
let data = null;
|
|
829
|
+
try {
|
|
830
|
+
data = await res.json();
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
if (controller.signal.aborted)
|
|
834
|
+
throw fail(0, 'timeout', `Legacy handoff timed out after ${timeoutMs}ms`);
|
|
835
|
+
data = null;
|
|
836
|
+
}
|
|
837
|
+
if (!res.ok) {
|
|
838
|
+
const { code, message } = serverErrorInfo(data);
|
|
839
|
+
throw fail(res.status, code || 'legacy_handoff_failed', message || `Legacy handoff failed: HTTP ${res.status}`, retryAfterHeaderSeconds(res));
|
|
840
|
+
}
|
|
841
|
+
const token = this.extractToken(res, data);
|
|
842
|
+
if (!token || typeof data?.user?.id !== 'string') {
|
|
843
|
+
throw fail(res.status, 'legacy_handoff_failed', 'Legacy handoff returned no session');
|
|
844
|
+
}
|
|
845
|
+
const answer = { token, user: data.user };
|
|
846
|
+
if (typeof data.api_key_description === 'string' && data.api_key_description)
|
|
847
|
+
answer.apiKeyDescription = data.api_key_description;
|
|
848
|
+
return answer;
|
|
798
849
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
throw new AuthenticationError({ type: 'auth_error', message: 'Legacy handoff returned no session', code: 'legacy_handoff_failed' });
|
|
850
|
+
finally {
|
|
851
|
+
clearTimeout(timer);
|
|
802
852
|
}
|
|
803
|
-
return { token, user: data.user };
|
|
804
853
|
}
|
|
805
|
-
/**
|
|
806
|
-
|
|
854
|
+
/**
|
|
855
|
+
* legacyHandoff, then the app API key, exactly like verifyOtpAndCreateKey.
|
|
856
|
+
* Pass the same `attemptId` (kept with the legacy session) on every call:
|
|
857
|
+
* a retry after the key step failed is then answered again instead of
|
|
858
|
+
* refused, and the key is made with the description the answer names.
|
|
859
|
+
*/
|
|
860
|
+
async legacyHandoffAndCreateKey(legacyToken, keyInput, options = {}) {
|
|
807
861
|
const session = await this.legacyHandoff(legacyToken, {
|
|
808
862
|
projectId: keyInput.projectId,
|
|
809
863
|
organizationId: keyInput.organizationId,
|
|
864
|
+
attemptId: options.attemptId,
|
|
865
|
+
refreshToken: options.refreshToken,
|
|
866
|
+
timeoutMs: options.timeoutMs,
|
|
810
867
|
});
|
|
811
|
-
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
868
|
+
const key = await this.createApiKeyOrRetireSession(session.apiKeyDescription && !keyInput.description ? { ...keyInput, description: session.apiKeyDescription } : keyInput, session);
|
|
812
869
|
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
813
870
|
}
|
|
814
871
|
/**
|
|
@@ -844,9 +901,13 @@ export class AuthResource {
|
|
|
844
901
|
method: 'POST',
|
|
845
902
|
credentials: 'include',
|
|
846
903
|
headers: {
|
|
904
|
+
// JSON Content-Type and body, as in signOut: without them Better Auth
|
|
905
|
+
// answers 415 before the endpoint runs.
|
|
906
|
+
'Content-Type': 'application/json',
|
|
847
907
|
...originHeader(this.baseOrigin),
|
|
848
908
|
Authorization: `Bearer ${sessionToken}`,
|
|
849
909
|
},
|
|
910
|
+
body: '{}',
|
|
850
911
|
});
|
|
851
912
|
let data = null;
|
|
852
913
|
try {
|
|
@@ -6,6 +6,7 @@ interface GatePromptMessage {
|
|
|
6
6
|
}
|
|
7
7
|
interface GatePromptInput {
|
|
8
8
|
messages: GatePromptMessage[];
|
|
9
|
+
/** @deprecated Ignored: the server always runs its own gate model. Still accepted so older callers validate. */
|
|
9
10
|
model?: string;
|
|
10
11
|
}
|
|
11
12
|
interface GatePromptResult {
|
|
@@ -20,7 +21,13 @@ interface Attachment {
|
|
|
20
21
|
interface ExecutePromptInput {
|
|
21
22
|
prompt: string;
|
|
22
23
|
current_screen?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Organization to run under (its AI provider settings and billing). Must be one
|
|
26
|
+
* you are a member of on your network that the API key may reach, or the
|
|
27
|
+
* request fails with 403.
|
|
28
|
+
*/
|
|
23
29
|
org_slug?: string | null;
|
|
30
|
+
/** @deprecated Ignored: the organization's AI provider settings, or the platform default, pick the model. Kept so older callers validate. */
|
|
24
31
|
model?: string;
|
|
25
32
|
locale?: string;
|
|
26
33
|
attachments?: Attachment[];
|
|
@@ -32,7 +39,13 @@ interface ExecutePromptResult {
|
|
|
32
39
|
export declare class CommandsResource {
|
|
33
40
|
private client;
|
|
34
41
|
constructor(client: HttpClient);
|
|
35
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Recursiv's own signup gate: evaluates a visitor's pitch for access to Recursiv.
|
|
44
|
+
* Not a moderation or guardrail API. It answers only for keys on the platform's
|
|
45
|
+
* networks (PLATFORM_SIGNUP_NETWORK_SLUGS) and returns 404 anywhere else, including
|
|
46
|
+
* an app's own network. Calls that reach the model share one global daily budget
|
|
47
|
+
* (GATE_PROMPT_DAILY_LLM_CALLS) with Recursiv's signup page.
|
|
48
|
+
*/
|
|
36
49
|
gatePrompt(input: GatePromptInput): Promise<SingleResponse<GatePromptResult>>;
|
|
37
50
|
/** Execute a command prompt with AI */
|
|
38
51
|
execute(input: ExecutePromptInput): Promise<SingleResponse<ExecutePromptResult>>;
|
|
@@ -3,7 +3,13 @@ export class CommandsResource {
|
|
|
3
3
|
constructor(client) {
|
|
4
4
|
this.client = client;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Recursiv's own signup gate: evaluates a visitor's pitch for access to Recursiv.
|
|
8
|
+
* Not a moderation or guardrail API. It answers only for keys on the platform's
|
|
9
|
+
* networks (PLATFORM_SIGNUP_NETWORK_SLUGS) and returns 404 anywhere else, including
|
|
10
|
+
* an app's own network. Calls that reach the model share one global daily budget
|
|
11
|
+
* (GATE_PROMPT_DAILY_LLM_CALLS) with Recursiv's signup page.
|
|
12
|
+
*/
|
|
7
13
|
gatePrompt(input) {
|
|
8
14
|
return this.client.post('/commands/gate-prompt', input);
|
|
9
15
|
}
|
package/dist/resources/e2ee.d.ts
CHANGED
|
@@ -11,6 +11,15 @@ export interface E2eeDevice {
|
|
|
11
11
|
isRoot: boolean;
|
|
12
12
|
status: 'active' | 'revoked' | string;
|
|
13
13
|
revokedAt: string | null;
|
|
14
|
+
/** When the relay registered the device (the relay's clock). Absent on relays before 0.7.57. */
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
/**
|
|
17
|
+
* When the device last fetched events, to the hour. Only in your own
|
|
18
|
+
* directory (`devices()` without `user_id`); null for another account's
|
|
19
|
+
* devices, when unknown, or when the relay cannot say. Absent on relays
|
|
20
|
+
* before 0.7.57.
|
|
21
|
+
*/
|
|
22
|
+
lastSeenAt?: string | null;
|
|
14
23
|
}
|
|
15
24
|
/** Opaque `recursiv-mls-v2-draft1` envelope with base64 MLS bytes. */
|
|
16
25
|
export interface E2eeEnvelope {
|
|
@@ -30,6 +39,33 @@ export interface E2eeEvent {
|
|
|
30
39
|
welcome: string | null;
|
|
31
40
|
createdAt: string;
|
|
32
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The realtime `e2ee:event` doorbell (`r.realtime.onE2eeEvent`), sent after
|
|
44
|
+
* every append to an encrypted conversation to each member account's sockets.
|
|
45
|
+
* It never carries plaintext and has no ordering authority.
|
|
46
|
+
*
|
|
47
|
+
* `event` is present only for a small application message (an ordinary or a
|
|
48
|
+
* quiet one; never a commit, never a Welcome, never one whose envelope is over
|
|
49
|
+
* 16 KB), and only on a socket whose credential could read it with `events()`:
|
|
50
|
+
* one authenticated by the account owner's session or by an owner key whose
|
|
51
|
+
* app or organization binding reaches the conversation. A socket opened with
|
|
52
|
+
* an agent-scoped key gets the content-free form only. When present it is the
|
|
53
|
+
* stored event exactly as `events()` returns it, JSON byte for byte.
|
|
54
|
+
*
|
|
55
|
+
* With the shared engine, hand `event` to `applyPushed(conversationId, event)`
|
|
56
|
+
* on the device session: it applies the event without a relay read when it is
|
|
57
|
+
* the next one this device expects, and syncs otherwise. Without `event`, sync.
|
|
58
|
+
*/
|
|
59
|
+
export interface E2eeRealtimeEvent {
|
|
60
|
+
conversationId: string;
|
|
61
|
+
/** The relay sequence the append was stored at. */
|
|
62
|
+
seq: number;
|
|
63
|
+
/** The stored event, when this socket may carry it (see above). */
|
|
64
|
+
event?: E2eeEvent & {
|
|
65
|
+
kind: 'application';
|
|
66
|
+
welcome: null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
33
69
|
/** One-time key packages a device still has to hand out. */
|
|
34
70
|
export interface E2eeKeyPackageStatus {
|
|
35
71
|
/** Unclaimed packages that have not expired. Once this reaches 0 nobody can add the device to a new conversation. */
|
|
@@ -37,12 +73,6 @@ export interface E2eeKeyPackageStatus {
|
|
|
37
73
|
/** Of `available`, how many expire within 7 days. */
|
|
38
74
|
expiringSoon: number;
|
|
39
75
|
}
|
|
40
|
-
/**
|
|
41
|
-
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
42
|
-
* credentials only; encryption happens in the shared core on each device.
|
|
43
|
-
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
44
|
-
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
45
|
-
*/
|
|
46
76
|
/** A reserved encrypted attachment object: PUT exactly `length` bytes of ciphertext to `uploadUrl`, then complete it. */
|
|
47
77
|
export interface E2eeObjectUpload {
|
|
48
78
|
reference: string;
|
|
@@ -56,9 +86,111 @@ export interface E2eeObjectDownload {
|
|
|
56
86
|
downloadUrl: string;
|
|
57
87
|
length: number;
|
|
58
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Why this account cannot use the E2EE relay on its network:
|
|
91
|
+
* - `network_off`: the network has not turned E2EE v2 on.
|
|
92
|
+
* - `not_allowlisted`: it is on, but only for pilot accounts, and this is not one.
|
|
93
|
+
*/
|
|
94
|
+
export type E2eeStatusReason = 'network_off' | 'not_allowlisted';
|
|
95
|
+
/** Whether the calling account is admitted to the E2EE relay on its network. */
|
|
96
|
+
export interface E2eeStatus {
|
|
97
|
+
/**
|
|
98
|
+
* True: this account may enroll devices, publish key packages, send, and
|
|
99
|
+
* start encryption in an eligible direct conversation. False: the relay is
|
|
100
|
+
* frozen for it. Conversations that already carry encryption stay readable
|
|
101
|
+
* (fetch, acknowledge, object downloads), its own devices can be listed and
|
|
102
|
+
* revoked, and nothing new can be sent or enrolled. It never falls back to
|
|
103
|
+
* plaintext.
|
|
104
|
+
*/
|
|
105
|
+
enabled: boolean;
|
|
106
|
+
/** Present only when `enabled` is false. Treat any unrecognised reason as off. */
|
|
107
|
+
reason?: E2eeStatusReason;
|
|
108
|
+
/**
|
|
109
|
+
* True when this relay can link a new device by scanning (`createLink`).
|
|
110
|
+
* False or absent: offer the codes fallback only as a choice the person
|
|
111
|
+
* makes, never switch to it on their behalf.
|
|
112
|
+
*/
|
|
113
|
+
links?: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** A device link's state. An open link past `expiresAt` reads as `cancelled` with `cancelledBy: 'expired'`. */
|
|
116
|
+
export type E2eeLinkState = 'open' | 'approved' | 'completed' | 'cancelled';
|
|
117
|
+
/** Why a link was cancelled. */
|
|
118
|
+
export type E2eeLinkCancelledBy = 'creator' | 'superseded' | 'declined' | 'expired';
|
|
119
|
+
/** How far the approving device has got adding the new device to the account's encrypted chats. */
|
|
120
|
+
export interface E2eeLinkProgress {
|
|
121
|
+
done: number;
|
|
122
|
+
total: number;
|
|
123
|
+
joined: number;
|
|
124
|
+
notHeld: number;
|
|
125
|
+
failed: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A device link (spec RCV1): the relay's short-lived mailbox between a new
|
|
129
|
+
* device, which shows a QR code, and an approved device of the same account,
|
|
130
|
+
* which scans it and posts an approval only the new device can verify. The
|
|
131
|
+
* relay stores public data only and is not trusted for integrity: the engine
|
|
132
|
+
* checks the approval's tag, the chain and the directory itself.
|
|
133
|
+
*/
|
|
134
|
+
export interface E2eeLink {
|
|
135
|
+
/** 128 random bits as unpadded base64url (22 characters); the QR carries these bytes. */
|
|
136
|
+
id: string;
|
|
137
|
+
state: E2eeLinkState;
|
|
138
|
+
deviceName: string;
|
|
139
|
+
signingKey: string;
|
|
140
|
+
/** The new device's self-reported label. Chosen by whoever made the link: never show it as the app's own words. */
|
|
141
|
+
label: string | null;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
expiresAt: string;
|
|
144
|
+
/** The relay's clock when it answered: count down with `expiresAt - now`, never the device's own clock. */
|
|
145
|
+
now: string;
|
|
146
|
+
scannedAt: string | null;
|
|
147
|
+
/** Another link of this account is open (another sign-in is linking too). */
|
|
148
|
+
otherOpen: boolean;
|
|
149
|
+
cancelledBy: E2eeLinkCancelledBy | null;
|
|
150
|
+
/** The approval body and its tag: only for the sign-in that created the link, null for everyone else. */
|
|
151
|
+
approval: string | null;
|
|
152
|
+
tag: string | null;
|
|
153
|
+
approverDeviceId: string | null;
|
|
154
|
+
progress: E2eeLinkProgress | null;
|
|
155
|
+
lastConversationId: string | null;
|
|
156
|
+
needPackages: boolean;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The realtime `e2ee:link` event (`r.realtime.onE2eeLink`): a link changed.
|
|
160
|
+
* Content-free; read the link with `link(id)`. There is never an `open` event.
|
|
161
|
+
*/
|
|
162
|
+
export interface E2eeLinkRealtimeEvent {
|
|
163
|
+
linkId: string;
|
|
164
|
+
state: 'scanned' | 'approved' | 'completed' | 'progress' | 'cancelled';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The realtime `e2ee:devices` event (`r.realtime.onE2eeDevices`): `packages`
|
|
168
|
+
* when one of the account's devices can now be claimed (it published after
|
|
169
|
+
* having none, or after a claim for it failed); `revoked` when one was revoked.
|
|
170
|
+
*/
|
|
171
|
+
export interface E2eeDevicesRealtimeEvent {
|
|
172
|
+
deviceId: string;
|
|
173
|
+
reason: 'packages' | 'revoked';
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
177
|
+
* credentials only; encryption happens in the shared core on each device.
|
|
178
|
+
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
179
|
+
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
180
|
+
*/
|
|
59
181
|
export declare class E2eeResource {
|
|
60
182
|
private client;
|
|
61
183
|
constructor(client: HttpClient);
|
|
184
|
+
/**
|
|
185
|
+
* Whether this account is admitted to encrypted chat on its network, without
|
|
186
|
+
* attempting a relay call. `{ enabled: true }`, or `{ enabled: false, reason }`
|
|
187
|
+
* with `reason` `'network_off'` or `'not_allowlisted'`. Only the caller's own
|
|
188
|
+
* account is described; the network comes from the key or session. Enrolling
|
|
189
|
+
* a device still needs an accepted two-person DM (or an already-encrypted
|
|
190
|
+
* conversation) in this key's scope. Agent-scoped keys get
|
|
191
|
+
* `agent_key_not_allowed`.
|
|
192
|
+
*/
|
|
193
|
+
status(): Promise<SingleResponse<E2eeStatus>>;
|
|
62
194
|
/** Register this device's public identity and its signed approval chain. */
|
|
63
195
|
registerDevice(input: {
|
|
64
196
|
device_name: string;
|
|
@@ -96,12 +228,28 @@ export declare class E2eeResource {
|
|
|
96
228
|
bytes: string;
|
|
97
229
|
device: E2eeDevice;
|
|
98
230
|
}>>;
|
|
231
|
+
/**
|
|
232
|
+
* Append one MLS envelope to an encrypted conversation.
|
|
233
|
+
*
|
|
234
|
+
* `silent: true` marks a message that changes an earlier one (a reaction,
|
|
235
|
+
* an edit, a delete for everyone): the relay sends no push (so no unread
|
|
236
|
+
* badge either) and does not move the conversation up the chat list. The
|
|
237
|
+
* `e2ee:event` doorbell still rings exactly as for any message: on the
|
|
238
|
+
* sockets of every member, the sender's own account included, except
|
|
239
|
+
* across a block. So every listener hears a quiet event, not only an open
|
|
240
|
+
* thread: a chat list that pulls on the doorbell pulls it too, and should
|
|
241
|
+
* leave the row where it is once it has decrypted a reaction, an edit or a
|
|
242
|
+
* delete. A commit ignores the flag. The relay learns only that the event
|
|
243
|
+
* is quiet; the flag is not stored. A relay that predates it drops the
|
|
244
|
+
* key, so the event is simply not quiet there.
|
|
245
|
+
*/
|
|
99
246
|
appendEvent(conversationId: string, input: {
|
|
100
247
|
device_id: string;
|
|
101
248
|
envelope: E2eeEnvelope;
|
|
102
249
|
welcome?: string;
|
|
103
250
|
welcome_for_device_id?: string;
|
|
104
251
|
expected_commit_seq?: number;
|
|
252
|
+
silent?: boolean;
|
|
105
253
|
}): Promise<SingleResponse<{
|
|
106
254
|
seq: number;
|
|
107
255
|
duplicate: boolean;
|
|
@@ -149,4 +297,71 @@ export declare class E2eeResource {
|
|
|
149
297
|
reference: string;
|
|
150
298
|
deleted: boolean;
|
|
151
299
|
}>>;
|
|
300
|
+
/**
|
|
301
|
+
* Start linking this new, not yet registered device: `POST /e2ee/links`.
|
|
302
|
+
* Answers the open link (180 s on the relay's clock). A new link cancels
|
|
303
|
+
* this sign-in's previous open link. `label` is 1-48 letters, numbers,
|
|
304
|
+
* spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
|
|
305
|
+
* Not retried: a 5xx is answered at once, and the caller decides.
|
|
306
|
+
*/
|
|
307
|
+
createLink(input: {
|
|
308
|
+
device_name: string;
|
|
309
|
+
signing_key: string;
|
|
310
|
+
label?: string;
|
|
311
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
312
|
+
/** Read a link: `GET /e2ee/links/:id`. `approval` and `tag` only for the sign-in that created it. Not retried: poll instead. */
|
|
313
|
+
link(id: string): Promise<SingleResponse<E2eeLink>>;
|
|
314
|
+
/** The approving device scanned the code: once per link, it keeps the link open 120 s past the scan. */
|
|
315
|
+
scannedLink(id: string, input: {
|
|
316
|
+
device_id: string;
|
|
317
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
318
|
+
/**
|
|
319
|
+
* Post the approval body and its tag, once. Retry-safe: the same approving
|
|
320
|
+
* device is answered 200 on an approved or completed link. The answer never
|
|
321
|
+
* carries the approval or tag.
|
|
322
|
+
*/
|
|
323
|
+
approveLink(id: string, input: {
|
|
324
|
+
device_id: string;
|
|
325
|
+
approval: string;
|
|
326
|
+
tag: string;
|
|
327
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
328
|
+
/** The new device, now registered, finished: the sign-in that created the link only. */
|
|
329
|
+
completeLink(id: string, input: {
|
|
330
|
+
device_id: string;
|
|
331
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
332
|
+
/** The approving device, from the sign-in that approved, reports how many chats it has added the new device to. */
|
|
333
|
+
linkProgress(id: string, input: {
|
|
334
|
+
device_id: string;
|
|
335
|
+
done: number;
|
|
336
|
+
total: number;
|
|
337
|
+
joined: number;
|
|
338
|
+
not_held: number;
|
|
339
|
+
failed: number;
|
|
340
|
+
last_conversation_id?: string;
|
|
341
|
+
need_packages?: boolean;
|
|
342
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
343
|
+
/**
|
|
344
|
+
* Cancel a link. The sign-in that created it cancels it while open or
|
|
345
|
+
* approved; an active device of the account declines an open one with
|
|
346
|
+
* `{ device_id, reason: 'declined' }`, and once the link is scanned only
|
|
347
|
+
* from the sign-in that scanned it.
|
|
348
|
+
*/
|
|
349
|
+
cancelLink(id: string, input?: {
|
|
350
|
+
device_id?: string;
|
|
351
|
+
reason?: 'cancelled' | 'declined';
|
|
352
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
353
|
+
/**
|
|
354
|
+
* The ids of the encrypted conversations this account belongs to, in this
|
|
355
|
+
* key's scope, ordered by id: pass `next` as `after` for the next page
|
|
356
|
+
* (`limit` 1-200, default 100).
|
|
357
|
+
*/
|
|
358
|
+
conversations(params?: {
|
|
359
|
+
after?: string;
|
|
360
|
+
limit?: number;
|
|
361
|
+
}): Promise<{
|
|
362
|
+
data: {
|
|
363
|
+
conversationId: string;
|
|
364
|
+
}[];
|
|
365
|
+
next: string | null;
|
|
366
|
+
}>;
|
|
152
367
|
}
|
package/dist/resources/e2ee.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
3
|
+
* credentials only; encryption happens in the shared core on each device.
|
|
4
|
+
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
5
|
+
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
6
|
+
*/
|
|
1
7
|
export class E2eeResource {
|
|
2
8
|
client;
|
|
3
9
|
constructor(client) {
|
|
4
10
|
this.client = client;
|
|
5
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether this account is admitted to encrypted chat on its network, without
|
|
14
|
+
* attempting a relay call. `{ enabled: true }`, or `{ enabled: false, reason }`
|
|
15
|
+
* with `reason` `'network_off'` or `'not_allowlisted'`. Only the caller's own
|
|
16
|
+
* account is described; the network comes from the key or session. Enrolling
|
|
17
|
+
* a device still needs an accepted two-person DM (or an already-encrypted
|
|
18
|
+
* conversation) in this key's scope. Agent-scoped keys get
|
|
19
|
+
* `agent_key_not_allowed`.
|
|
20
|
+
*/
|
|
21
|
+
status() {
|
|
22
|
+
return this.client.get('/e2ee/status');
|
|
23
|
+
}
|
|
6
24
|
/** Register this device's public identity and its signed approval chain. */
|
|
7
25
|
registerDevice(input) {
|
|
8
26
|
return this.client.post('/e2ee/devices', input);
|
|
@@ -28,6 +46,21 @@ export class E2eeResource {
|
|
|
28
46
|
claimKeyPackage(input) {
|
|
29
47
|
return this.client.post('/e2ee/key-packages/claim', input);
|
|
30
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Append one MLS envelope to an encrypted conversation.
|
|
51
|
+
*
|
|
52
|
+
* `silent: true` marks a message that changes an earlier one (a reaction,
|
|
53
|
+
* an edit, a delete for everyone): the relay sends no push (so no unread
|
|
54
|
+
* badge either) and does not move the conversation up the chat list. The
|
|
55
|
+
* `e2ee:event` doorbell still rings exactly as for any message: on the
|
|
56
|
+
* sockets of every member, the sender's own account included, except
|
|
57
|
+
* across a block. So every listener hears a quiet event, not only an open
|
|
58
|
+
* thread: a chat list that pulls on the doorbell pulls it too, and should
|
|
59
|
+
* leave the row where it is once it has decrypted a reaction, an edit or a
|
|
60
|
+
* delete. A commit ignores the flag. The relay learns only that the event
|
|
61
|
+
* is quiet; the flag is not stored. A relay that predates it drops the
|
|
62
|
+
* key, so the event is simply not quiet there.
|
|
63
|
+
*/
|
|
31
64
|
appendEvent(conversationId, input) {
|
|
32
65
|
// Retries are safe: the server is idempotent on (conversation, device, event id).
|
|
33
66
|
return this.client.post(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, input, { retry: true });
|
|
@@ -58,4 +91,61 @@ export class E2eeResource {
|
|
|
58
91
|
deleteObject(objectId, params) {
|
|
59
92
|
return this.client.delete(`/e2ee/objects/${encodeURIComponent(objectId)}?device_id=${encodeURIComponent(params.device_id)}`);
|
|
60
93
|
}
|
|
94
|
+
// ── Device links (spec RCV1 §6) ──
|
|
95
|
+
// Names match the engine's optional transport methods, so `r.e2ee` can be
|
|
96
|
+
// passed to the engine as its transport unchanged. Link refusals are 409
|
|
97
|
+
// (`link_limit`, `no_root`, `already_registered`, `conflict`), never 429;
|
|
98
|
+
// `invalid_label` is 400; `storage_unavailable` is a temporary 503: keep
|
|
99
|
+
// polling until the link's `expiresAt`.
|
|
100
|
+
/**
|
|
101
|
+
* Start linking this new, not yet registered device: `POST /e2ee/links`.
|
|
102
|
+
* Answers the open link (180 s on the relay's clock). A new link cancels
|
|
103
|
+
* this sign-in's previous open link. `label` is 1-48 letters, numbers,
|
|
104
|
+
* spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
|
|
105
|
+
* Not retried: a 5xx is answered at once, and the caller decides.
|
|
106
|
+
*/
|
|
107
|
+
createLink(input) {
|
|
108
|
+
return this.client.post('/e2ee/links', input, { retry: false });
|
|
109
|
+
}
|
|
110
|
+
/** Read a link: `GET /e2ee/links/:id`. `approval` and `tag` only for the sign-in that created it. Not retried: poll instead. */
|
|
111
|
+
link(id) {
|
|
112
|
+
return this.client.get(`/e2ee/links/${encodeURIComponent(id)}`, undefined, { retry: false });
|
|
113
|
+
}
|
|
114
|
+
/** The approving device scanned the code: once per link, it keeps the link open 120 s past the scan. */
|
|
115
|
+
scannedLink(id, input) {
|
|
116
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/scanned`, input);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Post the approval body and its tag, once. Retry-safe: the same approving
|
|
120
|
+
* device is answered 200 on an approved or completed link. The answer never
|
|
121
|
+
* carries the approval or tag.
|
|
122
|
+
*/
|
|
123
|
+
approveLink(id, input) {
|
|
124
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/approve`, input, { retry: true });
|
|
125
|
+
}
|
|
126
|
+
/** The new device, now registered, finished: the sign-in that created the link only. */
|
|
127
|
+
completeLink(id, input) {
|
|
128
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/complete`, input);
|
|
129
|
+
}
|
|
130
|
+
/** The approving device, from the sign-in that approved, reports how many chats it has added the new device to. */
|
|
131
|
+
linkProgress(id, input) {
|
|
132
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/progress`, input);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Cancel a link. The sign-in that created it cancels it while open or
|
|
136
|
+
* approved; an active device of the account declines an open one with
|
|
137
|
+
* `{ device_id, reason: 'declined' }`, and once the link is scanned only
|
|
138
|
+
* from the sign-in that scanned it.
|
|
139
|
+
*/
|
|
140
|
+
cancelLink(id, input) {
|
|
141
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/cancel`, input ?? {});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The ids of the encrypted conversations this account belongs to, in this
|
|
145
|
+
* key's scope, ordered by id: pass `next` as `after` for the next page
|
|
146
|
+
* (`limit` 1-200, default 100).
|
|
147
|
+
*/
|
|
148
|
+
conversations(params) {
|
|
149
|
+
return this.client.get('/e2ee/conversations', params);
|
|
150
|
+
}
|
|
61
151
|
}
|
|
@@ -15,6 +15,7 @@ export interface RequestHumanInputInput {
|
|
|
15
15
|
export interface HumanInputRequest {
|
|
16
16
|
conversation_id: string;
|
|
17
17
|
message_id: string;
|
|
18
|
+
human_ask_id: string | null;
|
|
18
19
|
/** Where to read the answer. MCP is stateless, so the agent polls. */
|
|
19
20
|
poll: string;
|
|
20
21
|
poll_after_seconds: number;
|
|
@@ -45,7 +46,9 @@ export declare class HumanInputResource {
|
|
|
45
46
|
private client;
|
|
46
47
|
constructor(client: HttpClient);
|
|
47
48
|
/** Raise a blocking question and notify the human wherever they are. */
|
|
48
|
-
request(input: RequestHumanInputInput
|
|
49
|
+
request(input: RequestHumanInputInput, options?: {
|
|
50
|
+
requestId?: string;
|
|
51
|
+
}): Promise<SingleResponse<HumanInputRequest>>;
|
|
49
52
|
/** Has the human answered yet? */
|
|
50
53
|
answers(conversationId: string): Promise<SingleResponse<HumanInputAnswers>>;
|
|
51
54
|
}
|
|
@@ -17,9 +17,13 @@ export class HumanInputResource {
|
|
|
17
17
|
this.client = client;
|
|
18
18
|
}
|
|
19
19
|
/** Raise a blocking question and notify the human wherever they are. */
|
|
20
|
-
request(input) {
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
request(input, options) {
|
|
21
|
+
// Never automatically replay: chat delivery may have committed before a
|
|
22
|
+
// response was lost, even when a keyed workspace ask was recorded once.
|
|
23
|
+
// Callers can reuse a requestId with the exact same input after an
|
|
24
|
+
// uncertain response. The server claims it against the durable
|
|
25
|
+
// organization-owned ask row; changed fields need a new requestId.
|
|
26
|
+
return this.client.post('/human-input', input, { retry: false, idempotencyKey: options?.requestId });
|
|
23
27
|
}
|
|
24
28
|
/** Has the human answered yet? */
|
|
25
29
|
answers(conversationId) {
|