@recursiv/sdk 0.7.69 → 0.7.70

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 CHANGED
@@ -75,7 +75,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
75
75
  | Resource | Key methods |
76
76
  |----------|------------|
77
77
  | `r.agents` | `create`, `update`, `chat`, `chatStream`, `retryChatStream`, `grantProjectAccess`, `conversations` |
78
- | `r.e2ee` | `status`, `registerDevice`, `devices`, `revokeDevice` (`{ cascade }` to also revoke devices approved through it; 409 `has_dependents` otherwise), `requestResetCode` and `resetAccount` (reset encrypted chat with an emailed code where `status().accountReset`), `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge`, `conversations`; device linking by scan: `createLink`, `link`, `scannedLink`, `approveLink`, `completeLink`, `linkProgress`, `cancelLink` (flag-gated E2EE v2 transport; its doorbell is `r.realtime.onE2eeEvent`, which carries a small message's stored ciphertext to the owner's own sockets; link and device changes arrive on `r.realtime.onE2eeLink` and `onE2eeDevices`) |
78
+ | `r.e2ee` | `status`, `registerDevice`, `devices`, `revokeDevice` (`{ cascade }` to also revoke devices approved through it; 409 `has_dependents` otherwise), `requestResetCode` and `resetAccount` (reset encrypted chat with an emailed code where `status().accountReset`), `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge`, `conversations`, `requestStart` and `cancelStartRequest` (ask the other person to encrypt a chat when they have no device yet: a message with `metadata.kind` `e2ee_start_request`; the live ones are `ConversationDetail.encryption_requests`); device linking by scan: `createLink`, `link`, `scannedLink`, `approveLink`, `completeLink`, `linkProgress`, `cancelLink` (flag-gated E2EE v2 transport; its doorbell is `r.realtime.onE2eeEvent`, which carries a small message's stored ciphertext to the owner's own sockets; link and device changes arrive on `r.realtime.onE2eeLink` and `onE2eeDevices`) |
79
79
  | `r.brain` | `sendMessage` |
80
80
  | `r.commands` | `execute`, `gatePrompt` |
81
81
  | `r.projectBrain` | `tasks`, `decisions`, `milestones`, `completeTask`, `usage` |
package/dist/index.d.ts CHANGED
@@ -158,7 +158,7 @@ export { UsersResource } from './resources/users.js';
158
158
  export { CommunitiesResource } from './resources/communities.js';
159
159
  export { ChatResource } from './resources/chat.js';
160
160
  export { E2eeResource } from './resources/e2ee.js';
161
- export type { E2eeAccountReset, E2eeDevice, E2eeDevicesRealtimeEvent, E2eeEnvelope, E2eeEvent, E2eeHasDependentsDetails, E2eeKeyPackageStatus, E2eeLink, E2eeLinkCancelledBy, E2eeLinkProgress, E2eeLinkRealtimeEvent, E2eeLinkState, E2eeObjectDownload, E2eeObjectUpload, E2eeRealtimeEvent, E2eeResetCodeSent, E2eeResetErrorCode, E2eeRevokeResponse, E2eeStatus, E2eeStatusReason, } from './resources/e2ee.js';
161
+ export type { E2eeAccountReset, E2eeDevice, E2eeDevicesRealtimeEvent, E2eeEnvelope, E2eeEvent, E2eeHasDependentsDetails, E2eeKeyPackageStatus, E2eeLink, E2eeLinkCancelledBy, E2eeLinkProgress, E2eeLinkRealtimeEvent, E2eeLinkState, E2eeObjectDownload, E2eeObjectUpload, E2eeRealtimeEvent, E2eeResetCodeSent, E2eeResetErrorCode, E2eeRevokeResponse, E2eeStartRequest, E2eeStatus, E2eeStatusReason, } from './resources/e2ee.js';
162
162
  export { ProjectsResource } from './resources/projects.js';
163
163
  export { AgentsResource, SUPPORTED_MODELS } from './resources/agents.js';
164
164
  export { EvidenceResource } from './resources/evidence.js';
@@ -14,6 +14,8 @@ export interface ChatMessageEvent {
14
14
  isAi?: boolean;
15
15
  };
16
16
  createdAt: string;
17
+ /** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request (MessageMetadata). */
18
+ metadata?: Record<string, unknown>;
17
19
  }
18
20
  export interface ChatMessageEditEvent {
19
21
  id: string;
@@ -163,11 +165,18 @@ export declare class RealtimeClient {
163
165
  onAgentThinking(callback: (event: AgentThinkingEvent) => void): (() => void) | undefined;
164
166
  /**
165
167
  * A private group's people changed (0.7.59): `{ conversationId, change, userIds, actorId }`,
166
- * change `added`, `removed`, `left` or `invites_cancelled`. Sent to everyone in the group
167
- * and to the people it is about. Read the conversation again; in an encrypted group, check its keys.
168
+ * change `added`, `accepted`, `removed`, `left` or `invites_cancelled`, sent to everyone in the group
169
+ * and to the people it is about; or `device` (a member's device can be added to the encrypted group
170
+ * now), sent to the group's admin only, on a connection that may read encrypted chat for the group.
171
+ * Read the conversation again; in an encrypted group, check its keys.
168
172
  */
169
173
  onChatMembers(callback: (event: ChatMembersEvent) => void): () => void;
170
- /** A private group was renamed (0.7.59): `{ conversationId, change: 'renamed', name, actorId }`. */
174
+ /**
175
+ * A conversation changed: a private group was renamed (0.7.59,
176
+ * `{ conversationId, change: 'renamed', name, actorId }`), or someone
177
+ * withdrew their request to encrypt a one-on-one chat
178
+ * (`encryption_request_withdrawn`, with `actorId`). Read it again.
179
+ */
171
180
  onChatConversation(callback: (event: ChatConversationEvent) => void): () => void;
172
181
  /**
173
182
  * The E2EE doorbell after each append to an encrypted conversation this
package/dist/realtime.js CHANGED
@@ -429,13 +429,20 @@ export class RealtimeClient {
429
429
  }
430
430
  /**
431
431
  * A private group's people changed (0.7.59): `{ conversationId, change, userIds, actorId }`,
432
- * change `added`, `removed`, `left` or `invites_cancelled`. Sent to everyone in the group
433
- * and to the people it is about. Read the conversation again; in an encrypted group, check its keys.
432
+ * change `added`, `accepted`, `removed`, `left` or `invites_cancelled`, sent to everyone in the group
433
+ * and to the people it is about; or `device` (a member's device can be added to the encrypted group
434
+ * now), sent to the group's admin only, on a connection that may read encrypted chat for the group.
435
+ * Read the conversation again; in an encrypted group, check its keys.
434
436
  */
435
437
  onChatMembers(callback) {
436
438
  return this.on('chat:members', callback);
437
439
  }
438
- /** A private group was renamed (0.7.59): `{ conversationId, change: 'renamed', name, actorId }`. */
440
+ /**
441
+ * A conversation changed: a private group was renamed (0.7.59,
442
+ * `{ conversationId, change: 'renamed', name, actorId }`), or someone
443
+ * withdrew their request to encrypt a one-on-one chat
444
+ * (`encryption_request_withdrawn`, with `actorId`). Read it again.
445
+ */
439
446
  onChatConversation(callback) {
440
447
  return this.on('chat:conversation', callback);
441
448
  }
@@ -93,7 +93,10 @@ export declare class ChatResource {
93
93
  * only stops listening for live events.
94
94
  */
95
95
  leave(conversationId: string): Promise<SingleResponse<LeaveConversationResult>>;
96
- /** Accept a message request (`request_status: 'pending'`). Idempotent; also reverses a decline. */
96
+ /**
97
+ * Accept a message request (`request_status: 'pending'`). Idempotent; also reverses a decline. In a
98
+ * private group, everyone in it gets the realtime `chat:members` event (`accepted`); a repeat sends none.
99
+ */
97
100
  acceptRequest(conversationId: string): Promise<SingleResponse<ConversationRequestResult>>;
98
101
  /**
99
102
  * Decline a message request. The conversation leaves the caller's lists and
@@ -108,7 +108,10 @@ export class ChatResource {
108
108
  leave(conversationId) {
109
109
  return this.client.post(`/chat/conversations/${conversationId}/leave`);
110
110
  }
111
- /** Accept a message request (`request_status: 'pending'`). Idempotent; also reverses a decline. */
111
+ /**
112
+ * Accept a message request (`request_status: 'pending'`). Idempotent; also reverses a decline. In a
113
+ * private group, everyone in it gets the realtime `chat:members` event (`accepted`); a repeat sends none.
114
+ */
112
115
  acceptRequest(conversationId) {
113
116
  return this.client.post(`/chat/conversations/${conversationId}/accept`);
114
117
  }
@@ -232,6 +232,16 @@ export interface E2eeHasDependentsDetails {
232
232
  dependents: string[];
233
233
  root: boolean;
234
234
  }
235
+ /**
236
+ * A live "Encrypt this chat" request (`e2ee.requestStart`): the message that
237
+ * carries it in the chat, and when it was made, by the relay's clock.
238
+ */
239
+ export interface E2eeStartRequest {
240
+ message_id: string;
241
+ requested_at: string;
242
+ /** False when this request was live already, so nothing was sent. Absent from older relays. */
243
+ created?: boolean;
244
+ }
235
245
  /** `revokeDevice`'s answer: the device, and every device the call revoked (the target first). */
236
246
  export interface E2eeRevokeResponse extends SingleResponse<E2eeDevice> {
237
247
  /** Absent on relays before 0.7.58. Empty when the device was already revoked and nothing else was. */
@@ -467,6 +477,37 @@ export declare class E2eeResource {
467
477
  device_id?: string;
468
478
  reason?: 'cancelled' | 'declined';
469
479
  }): Promise<SingleResponse<E2eeLink>>;
480
+ /**
481
+ * "Encrypt this chat" when the other person has no encrypted-chat device
482
+ * yet. The relay writes the request into the chat as a message from this
483
+ * account, with `metadata: { kind: 'e2ee_start_request', v: 1 }` (see
484
+ * `Message.metadata`), so it reaches the other person as a message does:
485
+ * their open chat, chat list and unread count, and one push, "<name> wants
486
+ * to encrypt your chat" (at most one a day per chat). Their device then
487
+ * starts the encryption as usual, with this account's devices. So this
488
+ * account needs an active device first (`no_root`, 409, otherwise).
489
+ *
490
+ * Answers this account's live request, `{ message_id, requested_at,
491
+ * created }`; asking again changes nothing and sends nothing (`created`
492
+ * false). The other person's app finds live requests in
493
+ * `chat.conversation()`'s `encryption_requests`. Refused where encryption
494
+ * cannot start: `conversation_not_eligible` (409), `blocked` (403),
495
+ * `forbidden` (403: not a member this key reaches); `conflict` (409) when
496
+ * the chat is already encrypted; `request_limit` (409, with
497
+ * `details.retryAfterSeconds`) after three requests in a day in this chat.
498
+ * Relays without it answer 404.
499
+ */
500
+ requestStart(conversationId: string): Promise<SingleResponse<E2eeStartRequest>>;
501
+ /**
502
+ * Withdraw this account's request to encrypt the chat: `{ cancelled }`,
503
+ * false when none of its requests was live (answered by a start, or
504
+ * withdrawn already). The request leaves the chat's history; open chats get
505
+ * `chat_message_delete`, and both people's sockets `chat:conversation`
506
+ * `encryption_request_withdrawn`. Relays without it answer 404.
507
+ */
508
+ cancelStartRequest(conversationId: string): Promise<SingleResponse<{
509
+ cancelled: boolean;
510
+ }>>;
470
511
  /**
471
512
  * The ids of the encrypted conversations this account belongs to, in this
472
513
  * key's scope, ordered by id: pass `next` as `after` for the next page
@@ -178,6 +178,39 @@ export class E2eeResource {
178
178
  cancelLink(id, input) {
179
179
  return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/cancel`, input ?? {});
180
180
  }
181
+ /**
182
+ * "Encrypt this chat" when the other person has no encrypted-chat device
183
+ * yet. The relay writes the request into the chat as a message from this
184
+ * account, with `metadata: { kind: 'e2ee_start_request', v: 1 }` (see
185
+ * `Message.metadata`), so it reaches the other person as a message does:
186
+ * their open chat, chat list and unread count, and one push, "<name> wants
187
+ * to encrypt your chat" (at most one a day per chat). Their device then
188
+ * starts the encryption as usual, with this account's devices. So this
189
+ * account needs an active device first (`no_root`, 409, otherwise).
190
+ *
191
+ * Answers this account's live request, `{ message_id, requested_at,
192
+ * created }`; asking again changes nothing and sends nothing (`created`
193
+ * false). The other person's app finds live requests in
194
+ * `chat.conversation()`'s `encryption_requests`. Refused where encryption
195
+ * cannot start: `conversation_not_eligible` (409), `blocked` (403),
196
+ * `forbidden` (403: not a member this key reaches); `conflict` (409) when
197
+ * the chat is already encrypted; `request_limit` (409, with
198
+ * `details.retryAfterSeconds`) after three requests in a day in this chat.
199
+ * Relays without it answer 404.
200
+ */
201
+ requestStart(conversationId) {
202
+ return this.client.put(`/e2ee/conversations/${encodeURIComponent(conversationId)}/start-request`, {});
203
+ }
204
+ /**
205
+ * Withdraw this account's request to encrypt the chat: `{ cancelled }`,
206
+ * false when none of its requests was live (answered by a start, or
207
+ * withdrawn already). The request leaves the chat's history; open chats get
208
+ * `chat_message_delete`, and both people's sockets `chat:conversation`
209
+ * `encryption_request_withdrawn`. Relays without it answer 404.
210
+ */
211
+ cancelStartRequest(conversationId) {
212
+ return this.client.delete(`/e2ee/conversations/${encodeURIComponent(conversationId)}/start-request`);
213
+ }
181
214
  /**
182
215
  * The ids of the encrypted conversations this account belongs to, in this
183
216
  * key's scope, ordered by id: pass `next` as `after` for the next page
package/dist/types.d.ts CHANGED
@@ -760,6 +760,25 @@ export interface ConversationDetail {
760
760
  * older APIs.
761
761
  */
762
762
  last_activity_at?: string;
763
+ /**
764
+ * The live "Encrypt this chat" requests (`e2ee.requestStart`) that can
765
+ * still be answered, oldest first, at most one per person, however far
766
+ * back in the history their messages are. Served by `conversation()` on a
767
+ * plaintext one-on-one chat only: `[]` when there is none (or the chat is
768
+ * not accepted by both, or a block stands). A request is listed only while
769
+ * both people are inside the encryption pilot and its sender has an active
770
+ * device. Absent on other chats and from older APIs.
771
+ */
772
+ encryption_requests?: ConversationEncryptionRequest[];
773
+ }
774
+ /** One entry of `ConversationDetail.encryption_requests`. */
775
+ export interface ConversationEncryptionRequest {
776
+ /** Who asked. The other person answers by starting encryption as usual, from their own device. */
777
+ sender_id: string;
778
+ /** The request's message (`metadata.kind` `e2ee_start_request`). */
779
+ message_id: string;
780
+ /** When it was asked, by the relay's clock. */
781
+ requested_at: string;
763
782
  }
764
783
  export type ChatMemberRole = 'admin' | 'member';
765
784
  /**
@@ -807,21 +826,39 @@ export interface UpdateGroupResult {
807
826
  * the conversation again (and, in an encrypted group, check its keys).
808
827
  * `actorId` is the admin for `added` and `removed`, and the leaver for `left`
809
828
  * and `invites_cancelled` (the invites an encrypted group's admin sent, which
810
- * went with them).
829
+ * went with them). `accepted`: the person in `userIds` (and `actorId`)
830
+ * accepted their invite; everyone in the group is told. `device`: one of that
831
+ * person's devices can be added to the encrypted group now (it has key
832
+ * packages after it had none); only the group's admin is told, since only the
833
+ * admin's device adds someone it invited, and only on a connection that may
834
+ * read encrypted chat for the group (a session, or a key for the group's app;
835
+ * never an agent's key). Read `change` defensively: an older API never sends
836
+ * the last two, and a newer one may add more.
811
837
  */
812
838
  export interface ChatMembersEvent {
813
839
  conversationId: string;
814
- change: 'added' | 'removed' | 'left' | 'invites_cancelled';
840
+ change: 'added' | 'accepted' | 'removed' | 'left' | 'invites_cancelled' | 'device';
815
841
  userIds: string[];
816
842
  actorId: string;
817
843
  }
818
- /** Realtime `chat:conversation`: a private group was renamed by its admin. */
819
- export interface ChatConversationEvent {
844
+ /**
845
+ * Realtime `chat:conversation`. Ids only, except a group's new name:
846
+ * - `renamed`: a private group was renamed by its admin (`name`).
847
+ * - `encryption_request_withdrawn`: `actorId` withdrew their request to
848
+ * encrypt this one-on-one chat (`e2ee.cancelStartRequest`); it left the
849
+ * history. Read the conversation and its messages again.
850
+ * Treat any other `change` as "read the conversation again".
851
+ */
852
+ export type ChatConversationEvent = {
820
853
  conversationId: string;
821
854
  change: 'renamed';
822
855
  name: string;
823
856
  actorId: string;
824
- }
857
+ } | {
858
+ conversationId: string;
859
+ change: 'encryption_request_withdrawn';
860
+ actorId: string;
861
+ };
825
862
  export interface MessageReaction {
826
863
  type: string;
827
864
  user_id: string;
@@ -834,6 +871,20 @@ export interface MessageMedia {
834
871
  mime_type: string | null;
835
872
  file_name: string | null;
836
873
  }
874
+ /**
875
+ * What a message the relay wrote on someone's behalf is, by `kind`. Only such
876
+ * messages carry it.
877
+ * - `e2ee_start_request` (`v` 1): the sender asked to turn on end-to-end
878
+ * encryption for this one-on-one chat (`e2ee.requestStart`). Show it as
879
+ * that request, not as its `content`, which is a fallback for apps that do
880
+ * not know the kind. It is live while it is in the history and the chat is
881
+ * not encrypted; a withdrawn one leaves the history. The person who did not
882
+ * ask answers it by starting encryption as usual, from their own device.
883
+ */
884
+ export interface MessageMetadata {
885
+ kind: 'e2ee_start_request';
886
+ v: number;
887
+ }
837
888
  export interface Message {
838
889
  id: string;
839
890
  content: string;
@@ -842,6 +893,8 @@ export interface Message {
842
893
  media: MessageMedia[];
843
894
  reactions: MessageReaction[];
844
895
  created_at: string;
896
+ /** Absent on a person's own message, and from older APIs: see MessageMetadata. */
897
+ metadata?: MessageMetadata;
845
898
  }
846
899
  export interface SendMessageInput {
847
900
  conversation_id: string;
@@ -861,6 +914,8 @@ export interface ChatMessageEvent {
861
914
  isAi?: boolean;
862
915
  };
863
916
  createdAt: string;
917
+ /** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request (MessageMetadata). */
918
+ metadata?: Record<string, unknown>;
864
919
  }
865
920
  export interface ChatMessageEditEvent {
866
921
  id: string;
package/llm.md CHANGED
@@ -522,6 +522,9 @@ await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); //
522
522
  await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope: reaction, silent: true }); // no push, no move up the chat list; every member's doorbell still rings
523
523
  const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
524
524
  await r.e2ee.acknowledge(conversationId, { device_id: device.id, seq: page.headSeq });
525
+ // The other person has no device yet: ask them (0.7.70). The relay writes it into the chat as a message with metadata { kind: 'e2ee_start_request', v: 1 }; their start answers it.
526
+ await r.e2ee.requestStart(conversationId); // → { message_id, requested_at, created }; cancelStartRequest(conversationId) withdraws it
527
+ // The live requests, however far back: (await r.chat.conversation(conversationId)).data.encryption_requests → [{ sender_id, message_id, requested_at }]
525
528
  // Doorbell after every append: { conversationId, seq, event? }. `event` (a small application message, as events() returns it) comes only to a socket of the owner's session or an owner key, never an agent-scoped key.
526
529
  // The policy is required: policyForConversationType(conversation.type) from @recursiv/e2ee ('direct' or 'group').
527
530
  r.realtime.onE2eeEvent(({ conversationId, event }) => (event ? controller.applyPushed(conversationId, event, policy) : controller.sync(conversationId, undefined, policy)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.69",
3
+ "version": "0.7.70",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",