@recursiv/sdk 0.7.57 → 0.7.59

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.
@@ -20,7 +20,54 @@ export interface E2eeDevice {
20
20
  * before 0.7.57.
21
21
  */
22
22
  lastSeenAt?: string | null;
23
+ /**
24
+ * Set on a root that a recorded reset of encrypted chat retired
25
+ * (`resetAccount`): when the old main device was revoked. Null on every
26
+ * other device, and on a root that stopped being the root without a
27
+ * recorded reset: treat that change as unexplained (compare security codes),
28
+ * never as a reset. Absent on relays without resets.
29
+ */
30
+ rootRetiredAt?: string | null;
31
+ }
32
+ /** `requestResetCode`'s answer: when the emailed code expires, and the masked address it went to. */
33
+ export interface E2eeResetCodeSent {
34
+ expires_at: string;
35
+ /** Masked, e.g. `b***@minds.com`: show it so the person knows which inbox to open. */
36
+ sent_to: string;
37
+ }
38
+ /** What `resetAccount` did. The account's encrypted chat starts again from `device`. */
39
+ export interface E2eeAccountReset {
40
+ reset: true;
41
+ resetAt: string;
42
+ /** The main device the reset retired; null when the account had none. */
43
+ retiredRoot: {
44
+ deviceId: string;
45
+ signingKey: string;
46
+ } | null;
47
+ /** Every device the reset revoked. */
48
+ revokedDeviceIds: string[];
49
+ /** This device, registered as the account's main device (root). */
50
+ device: E2eeDevice;
51
+ /**
52
+ * How many other sessions and API keys of the account were signed out, and
53
+ * how many AI clients connected over MCP OAuth were disconnected.
54
+ */
55
+ signedOut: {
56
+ sessions: number;
57
+ keys: number;
58
+ connectedApps: number;
59
+ };
23
60
  }
61
+ /**
62
+ * Codes a reset, or a call a reset may be needed for, can be refused with:
63
+ * - `confirmation_failed` (403): the code is wrong, expired, spent or dead after five wrong tries.
64
+ * - `code_limit` (409, never 429): too many codes asked for; `details.retryAfterSeconds`.
65
+ * - `no_email_on_file` (400), `email_delivery_unavailable` (503): no code could be sent.
66
+ * - `reset_required` (409): from `createLink` when the account's main device is gone with
67
+ * nothing left to approve a new one, and from `revokeDevice` on the main device. Only a reset goes on.
68
+ * - `conflict` (409): the device name or key was used before; start the device again with fresh ones.
69
+ */
70
+ export type E2eeResetErrorCode = 'confirmation_failed' | 'code_limit' | 'no_email_on_file' | 'email_delivery_unavailable' | 'reset_required' | 'conflict';
24
71
  /** Opaque `recursiv-mls-v2-draft1` envelope with base64 MLS bytes. */
25
72
  export interface E2eeEnvelope {
26
73
  profile: 'recursiv-mls-v2-draft1';
@@ -111,6 +158,12 @@ export interface E2eeStatus {
111
158
  * makes, never switch to it on their behalf.
112
159
  */
113
160
  links?: boolean;
161
+ /**
162
+ * True when this network offers this account resetting encrypted chat
163
+ * (`requestResetCode`, `resetAccount`); never for an account the reset would
164
+ * refuse, such as a platform owner's. False or absent: do not offer it.
165
+ */
166
+ accountReset?: boolean;
114
167
  }
115
168
  /** A device link's state. An open link past `expiresAt` reads as `cancelled` with `cancelledBy: 'expired'`. */
116
169
  export type E2eeLinkState = 'open' | 'approved' | 'completed' | 'cancelled';
@@ -163,6 +216,23 @@ export interface E2eeLinkRealtimeEvent {
163
216
  linkId: string;
164
217
  state: 'scanned' | 'approved' | 'completed' | 'progress' | 'cancelled';
165
218
  }
219
+ /**
220
+ * The `details` of a 409 `has_dependents` refusal from `revokeDevice` (read
221
+ * them from the thrown `ConflictError`'s `details`): the ids of the account's
222
+ * active devices that were approved through the device and would stop
223
+ * working with it, and whether it is the account's root (main) device, which
224
+ * every device chains to; once the root is revoked the account cannot link a
225
+ * new device.
226
+ */
227
+ export interface E2eeHasDependentsDetails {
228
+ dependents: string[];
229
+ root: boolean;
230
+ }
231
+ /** `revokeDevice`'s answer: the device, and every device the call revoked (the target first). */
232
+ export interface E2eeRevokeResponse extends SingleResponse<E2eeDevice> {
233
+ /** Absent on relays before 0.7.58. Empty when the device was already revoked and nothing else was. */
234
+ revoked?: string[];
235
+ }
166
236
  /**
167
237
  * The realtime `e2ee:devices` event (`r.realtime.onE2eeDevices`): `packages`
168
238
  * when one of the account's devices can now be claimed (it published after
@@ -203,7 +273,48 @@ export declare class E2eeResource {
203
273
  }): Promise<{
204
274
  data: E2eeDevice[];
205
275
  }>;
206
- revokeDevice(deviceId: string): Promise<SingleResponse<E2eeDevice>>;
276
+ /**
277
+ * Revoke one of your devices. When other active devices were approved
278
+ * through it, or it is the account's root (main) device, the relay changes
279
+ * nothing and throws a `ConflictError` with code `has_dependents` and
280
+ * `details: E2eeHasDependentsDetails`, so the app can say which devices
281
+ * would be signed out. Call again with `{ cascade: true }` to revoke it and
282
+ * every device chained through it (relays from 0.7.58; older relays ignore
283
+ * the body and revoke only the device). Where this account is offered
284
+ * resetting encrypted chat (`status().accountReset`), the main device is never
285
+ * revoked this way: 409 `reset_required`; replace it with `resetAccount`.
286
+ */
287
+ revokeDevice(deviceId: string, input?: {
288
+ cascade?: boolean;
289
+ }): Promise<E2eeRevokeResponse>;
290
+ /**
291
+ * Email this account an eight-digit code to reset encrypted chat, and warn
292
+ * its owner that one was asked for. Only where `status().accountReset` is
293
+ * true. The account's code budget is shared with other confirmation codes:
294
+ * over it, a `ConflictError` with code `code_limit` and
295
+ * `details.retryAfterSeconds` (never a 429). Not retried.
296
+ */
297
+ requestResetCode(): Promise<SingleResponse<E2eeResetCodeSent>>;
298
+ /**
299
+ * Reset encrypted chat with the emailed code, and make this device the
300
+ * account's main device: pass it as `registerDevice` takes it, with a fresh
301
+ * name and key and its own self-signed credential. Every other device of
302
+ * the account is removed from encrypted chat, and the account is signed out
303
+ * of every other session and API key on this network (`keep_session_token`
304
+ * keeps a Better Auth session this device holds). Contacts see that the
305
+ * security code changed. A wrong, expired or spent code: `confirmation_failed`
306
+ * (403), nothing changed. Sending the same request again after it succeeded
307
+ * (from the same sign-in) answers the same result, so a retry after a lost
308
+ * answer is safe.
309
+ */
310
+ resetAccount(input: {
311
+ email_code: string;
312
+ device_name: string;
313
+ realm: string;
314
+ signing_key: string;
315
+ credential: Record<string, unknown>;
316
+ keep_session_token?: string;
317
+ }): Promise<SingleResponse<E2eeAccountReset>>;
207
318
  /** Publish one-time key packages for one of your devices; `available` is how many it now has. */
208
319
  publishKeyPackages(input: {
209
320
  device_id: string;
@@ -300,7 +411,9 @@ export declare class E2eeResource {
300
411
  /**
301
412
  * Start linking this new, not yet registered device: `POST /e2ee/links`.
302
413
  * 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,
414
+ * this sign-in's previous open link. `reset_required` (409): the account's
415
+ * main device is gone and nothing is left to approve this one; only
416
+ * `resetAccount` goes on. `label` is 1-48 letters, numbers,
304
417
  * spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
305
418
  * Not retried: a 5xx is answered at once, and the caller decides.
306
419
  */
@@ -28,8 +28,44 @@ export class E2eeResource {
28
28
  devices(params) {
29
29
  return this.client.get('/e2ee/devices', params);
30
30
  }
31
- revokeDevice(deviceId) {
32
- return this.client.post(`/e2ee/devices/${encodeURIComponent(deviceId)}/revoke`);
31
+ /**
32
+ * Revoke one of your devices. When other active devices were approved
33
+ * through it, or it is the account's root (main) device, the relay changes
34
+ * nothing and throws a `ConflictError` with code `has_dependents` and
35
+ * `details: E2eeHasDependentsDetails`, so the app can say which devices
36
+ * would be signed out. Call again with `{ cascade: true }` to revoke it and
37
+ * every device chained through it (relays from 0.7.58; older relays ignore
38
+ * the body and revoke only the device). Where this account is offered
39
+ * resetting encrypted chat (`status().accountReset`), the main device is never
40
+ * revoked this way: 409 `reset_required`; replace it with `resetAccount`.
41
+ */
42
+ revokeDevice(deviceId, input) {
43
+ return this.client.post(`/e2ee/devices/${encodeURIComponent(deviceId)}/revoke`, input ?? {});
44
+ }
45
+ /**
46
+ * Email this account an eight-digit code to reset encrypted chat, and warn
47
+ * its owner that one was asked for. Only where `status().accountReset` is
48
+ * true. The account's code budget is shared with other confirmation codes:
49
+ * over it, a `ConflictError` with code `code_limit` and
50
+ * `details.retryAfterSeconds` (never a 429). Not retried.
51
+ */
52
+ requestResetCode() {
53
+ return this.client.post('/e2ee/account/reset-code', {}, { retry: false });
54
+ }
55
+ /**
56
+ * Reset encrypted chat with the emailed code, and make this device the
57
+ * account's main device: pass it as `registerDevice` takes it, with a fresh
58
+ * name and key and its own self-signed credential. Every other device of
59
+ * the account is removed from encrypted chat, and the account is signed out
60
+ * of every other session and API key on this network (`keep_session_token`
61
+ * keeps a Better Auth session this device holds). Contacts see that the
62
+ * security code changed. A wrong, expired or spent code: `confirmation_failed`
63
+ * (403), nothing changed. Sending the same request again after it succeeded
64
+ * (from the same sign-in) answers the same result, so a retry after a lost
65
+ * answer is safe.
66
+ */
67
+ resetAccount(input) {
68
+ return this.client.post('/e2ee/account/reset', input, { retry: true });
33
69
  }
34
70
  /** Publish one-time key packages for one of your devices; `available` is how many it now has. */
35
71
  publishKeyPackages(input) {
@@ -100,7 +136,9 @@ export class E2eeResource {
100
136
  /**
101
137
  * Start linking this new, not yet registered device: `POST /e2ee/links`.
102
138
  * 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,
139
+ * this sign-in's previous open link. `reset_required` (409): the account's
140
+ * main device is gone and nothing is left to approve this one; only
141
+ * `resetAccount` goes on. `label` is 1-48 letters, numbers,
104
142
  * spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
105
143
  * Not retried: a 5xx is answered at once, and the caller decides.
106
144
  */
@@ -112,15 +112,39 @@ export interface SendEmailInput {
112
112
  reply_to?: string;
113
113
  /** File attachments (max 10). Content must be base64-encoded. */
114
114
  attachments?: EmailAttachment[];
115
+ /**
116
+ * Defaults to `'marketing'`, which is held unless the recipient has given
117
+ * marketing consent. Pass `'transactional'` for receipts, invites,
118
+ * confirmations and alerts.
119
+ */
120
+ category?: 'transactional' | 'product' | 'marketing';
121
+ }
122
+ export interface SendEmailMessage {
123
+ id: string | null;
124
+ to: string;
125
+ /** `queued`/`sent`/`retry` are on their way; `skipped`/`suppressed`/`failed`/`duplicate` are not. */
126
+ status: 'queued' | 'sent' | 'retry' | 'failed' | 'suppressed' | 'skipped' | 'duplicate' | (string & {});
127
+ /** Why a held recipient was not sent, e.g. "marketing consent not granted". */
128
+ reason?: string;
115
129
  }
116
130
  export interface SendEmailResult {
117
- id: string;
118
- status: string;
131
+ id: string | null;
132
+ /** `queued`: every recipient; `partial`: some held (see `messages`). A send where all were held throws `EmailNotSentError`. */
133
+ status: 'queued' | 'partial' | 'skipped' | (string & {});
134
+ messages?: SendEmailMessage[];
135
+ /** Present when any recipient was held: says why, in words. */
136
+ hint?: string;
119
137
  }
120
138
  export declare class EmailResource {
121
139
  private client;
122
140
  constructor(client: HttpClient);
123
- /** Send a transactional email (uses org's BYOK Resend key if configured) */
141
+ /**
142
+ * Send an email as the organization (its BYOK Resend key when configured).
143
+ * Set `category: 'transactional'` for receipts, invites and alerts: the
144
+ * default is `'marketing'`, which needs the recipient's consent.
145
+ * Throws `EmailNotSentError` when every recipient was held; check
146
+ * `status === 'partial'` and `messages` for a send that reached only some.
147
+ */
124
148
  send(input: SendEmailInput): Promise<SingleResponse<SendEmailResult>>;
125
149
  /** List all campaigns */
126
150
  listCampaigns(): Promise<SingleResponse<CampaignSummary[]>>;
@@ -1,11 +1,26 @@
1
+ import { EmailNotSentError } from '../errors.js';
1
2
  export class EmailResource {
2
3
  client;
3
4
  constructor(client) {
4
5
  this.client = client;
5
6
  }
6
- /** Send a transactional email (uses org's BYOK Resend key if configured) */
7
- send(input) {
8
- return this.client.post('/email/send', input);
7
+ /**
8
+ * Send an email as the organization (its BYOK Resend key when configured).
9
+ * Set `category: 'transactional'` for receipts, invites and alerts: the
10
+ * default is `'marketing'`, which needs the recipient's consent.
11
+ * Throws `EmailNotSentError` when every recipient was held; check
12
+ * `status === 'partial'` and `messages` for a send that reached only some.
13
+ */
14
+ async send(input) {
15
+ const res = (await this.client.post('/email/send', input));
16
+ if (res?.data?.status === 'skipped') {
17
+ throw new EmailNotSentError({
18
+ message: 'Email not sent: every recipient was held.',
19
+ hint: res.data.hint,
20
+ details: res.data,
21
+ });
22
+ }
23
+ return res;
9
24
  }
10
25
  /** List all campaigns */
11
26
  listCampaigns() {
@@ -37,6 +37,25 @@ export interface DeleteEnvVarParams {
37
37
  /** Omit to remove the variable from every environment. */
38
38
  environment?: EnvVarEnvironment;
39
39
  }
40
+ export interface CreateProjectApiKeyInput {
41
+ project_id: string;
42
+ /** The env var that receives the key. UPPER_SNAKE_CASE, e.g. RECURSIV_API_KEY. */
43
+ key: string;
44
+ /** Defaults to databases:read + databases:write. Never more than the calling key holds; never admin. */
45
+ scopes?: string[];
46
+ /** Defaults to 'both'. */
47
+ environment?: EnvVarEnvironment;
48
+ }
49
+ export interface CreateProjectApiKeyResult {
50
+ key: string;
51
+ environment: EnvVarEnvironment;
52
+ api_key_id: string;
53
+ /** The key's public prefix, for recognizing it in a key list. Not the key. */
54
+ api_key_prefix: string;
55
+ scopes: string[];
56
+ /** How many earlier keys minted into this env var were revoked. */
57
+ revoked_previous: number;
58
+ }
40
59
  export declare class EnvVarsResource {
41
60
  private client;
42
61
  constructor(client: HttpClient);
@@ -47,6 +66,12 @@ export declare class EnvVarsResource {
47
66
  * changes what runs in production.
48
67
  */
49
68
  set(input: SetEnvVarInput): Promise<SingleResponse<Omit<EnvVar, 'value' | 'source' | 'updated_at'>>>;
69
+ /**
70
+ * Mint a project-bound API key straight into an env var. The key value goes
71
+ * to the encrypted variable and is never returned. Running it again rotates:
72
+ * the previous key in that variable is revoked. Requires project admin.
73
+ */
74
+ createProjectApiKey(input: CreateProjectApiKeyInput): Promise<SingleResponse<CreateProjectApiKeyResult>>;
50
75
  delete(params: DeleteEnvVarParams): Promise<SingleResponse<{
51
76
  key: string;
52
77
  deleted: boolean;
@@ -17,6 +17,14 @@ export class EnvVarsResource {
17
17
  async set(input) {
18
18
  return this.client.put('/env-vars', input);
19
19
  }
20
+ /**
21
+ * Mint a project-bound API key straight into an env var. The key value goes
22
+ * to the encrypted variable and is never returned. Running it again rotates:
23
+ * the previous key in that variable is revoked. Requires project admin.
24
+ */
25
+ async createProjectApiKey(input) {
26
+ return this.client.post('/env-vars/api-key', input);
27
+ }
20
28
  async delete(params) {
21
29
  const query = new URLSearchParams({ project_id: params.project_id, key: params.key });
22
30
  if (params.environment)
@@ -63,6 +63,18 @@ export interface ApplyModerationActionInput {
63
63
  actor_type?: 'human' | 'ai' | 'system';
64
64
  source_report_id?: string;
65
65
  }
66
+ /** What a granted agent may do on the report queue: shadow reads it; act also resolves and dismisses. */
67
+ export type AgentModerationMode = 'shadow' | 'act';
68
+ /** A network admin's grant letting one AI agent work the report queue. */
69
+ export interface AgentModerationGrant {
70
+ id: string;
71
+ network_id: string | null;
72
+ agent_id: string;
73
+ mode: AgentModerationMode;
74
+ granted_by_id: string | null;
75
+ created_at: string;
76
+ revoked_at: string | null;
77
+ }
66
78
  export declare class ModerationResource {
67
79
  private client;
68
80
  constructor(client: HttpClient);
@@ -101,4 +113,14 @@ export declare class ModerationResource {
101
113
  action_id: string;
102
114
  outcome: 'upheld' | 'overturned';
103
115
  }>>;
116
+ /** Admin: the network's active agent moderation grants. */
117
+ listAgentGrants(): Promise<SingleResponse<AgentModerationGrant[]>>;
118
+ /**
119
+ * Admin: let an AI agent in the caller's network work the report queue. An
120
+ * active grant is replaced. The agent's own key also needs the
121
+ * `moderation:agent` scope.
122
+ */
123
+ grantAgent(agentId: string, mode: AgentModerationMode): Promise<SingleResponse<AgentModerationGrant>>;
124
+ /** Admin: revoke an agent's active moderation grant. */
125
+ revokeAgent(agentId: string): Promise<SingleResponse<AgentModerationGrant>>;
104
126
  }
@@ -34,4 +34,20 @@ export class ModerationResource {
34
34
  resolveAppeal(appealId, input) {
35
35
  return this.client.post(`/moderation/appeals/${encodeURIComponent(appealId)}/resolve`, input);
36
36
  }
37
+ /** Admin: the network's active agent moderation grants. */
38
+ listAgentGrants() {
39
+ return this.client.get('/moderation/agent-grants');
40
+ }
41
+ /**
42
+ * Admin: let an AI agent in the caller's network work the report queue. An
43
+ * active grant is replaced. The agent's own key also needs the
44
+ * `moderation:agent` scope.
45
+ */
46
+ grantAgent(agentId, mode) {
47
+ return this.client.post('/moderation/agent-grants', { agent_id: agentId, mode });
48
+ }
49
+ /** Admin: revoke an agent's active moderation grant. */
50
+ revokeAgent(agentId) {
51
+ return this.client.delete(`/moderation/agent-grants/${encodeURIComponent(agentId)}`);
52
+ }
37
53
  }
@@ -17,7 +17,7 @@ export interface CategoryOrderMeta {
17
17
  export declare class PostsResource {
18
18
  private client;
19
19
  constructor(client: HttpClient);
20
- /** List posts with optional filters */
20
+ /** List posts with optional filters. `retry: false` asks once and is never sent (see ListPostsParams.retry). */
21
21
  list(params?: ListPostsParams): Promise<PaginatedResponse<Post>>;
22
22
  /**
23
23
  * Broad categories currently enabled for public posts in this app, in the order this member should see
@@ -3,9 +3,13 @@ export class PostsResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
- /** List posts with optional filters */
6
+ /** List posts with optional filters. `retry: false` asks once and is never sent (see ListPostsParams.retry). */
7
7
  list(params) {
8
- return this.client.get('/posts', params);
8
+ // Without `retry`, the same request older SDKs make.
9
+ if (!params || !('retry' in params))
10
+ return this.client.get('/posts', params);
11
+ const { retry, ...query } = params;
12
+ return retry === false ? this.client.get('/posts', query, { retry: false }) : this.client.get('/posts', query);
9
13
  }
10
14
  /**
11
15
  * Broad categories currently enabled for public posts in this app, in the order this member should see