@extrovert.dev/sdk 0.1.0-pre.3

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.
@@ -0,0 +1,3782 @@
1
+ /**
2
+ * fetch-based transport for the Extrovert API.
3
+ *
4
+ * Runtime-agnostic: uses the global `fetch` (Node 18+, Cloudflare Workers, Vercel Edge, Deno,
5
+ * browsers). No Node-only APIs, no dependencies. Retries idempotent requests with jittered backoff
6
+ * on 429/5xx, honors `Retry-After`, and surfaces every failure as a typed {@link ApiError}.
7
+ */
8
+ /** The library version, surfaced in the User-Agent. Kept in sync with package.json by build. */
9
+ declare const SDK_VERSION = "0.1.0-pre.3";
10
+ interface RetryOptions {
11
+ /** Max retry attempts for idempotent requests on 429/5xx/network errors. Default 2. */
12
+ maxRetries: number;
13
+ /** Base backoff in ms (grows exponentially with jitter). Default 250. */
14
+ baseDelayMs: number;
15
+ /** Cap on a single backoff delay. Default 8000. */
16
+ maxDelayMs: number;
17
+ }
18
+
19
+ /**
20
+ * `?include=` relation expansion typing (redesign §5.5 / §6.2).
21
+ *
22
+ * Reads may expand a per-resource allowlist of relations (depth ≤ 2); the server
23
+ * re-applies the same ceiling/ownership filter to every expanded child. The SDK
24
+ * types the allowed relations per resource so a caller cannot ask for a relation the
25
+ * server would reject with 400 `bad_request`, and serializes the list as the
26
+ * comma-separated `?include=` value.
27
+ */
28
+ /** Relations the `inbox` resource may expand (`?include=agent,domain`). */
29
+ type InboxInclude = "agent" | "domain";
30
+ /** Relations the `review` resource may expand (`?include=category,turns`). */
31
+ type ReviewInclude = "category" | "turns";
32
+ /** Serialize an include list into the comma-separated `?include=` query value. */
33
+ declare function serializeInclude(include?: readonly string[]): string | undefined;
34
+
35
+ /**
36
+ * Extrovert API — typed request/response models.
37
+ *
38
+ * These mirror the Extrovert V1 REST contract (`/v1`, §8 of the build spec). The Go API does not
39
+ * exist yet; field shapes here are the source of truth the client codes against and are validated
40
+ * against fixture data in `src/fixtures.ts`.
41
+ *
42
+ * Conventions:
43
+ * - All identifiers (ids, addresses, domains, keys) are `string` and rendered in mono everywhere.
44
+ * - Timestamps are RFC 3339 / ISO-8601 strings (`created_at`, `expires_at`, ...).
45
+ * - The wire format is snake_case to match the Go/OpenAPI surface; this file documents it verbatim.
46
+ */
47
+
48
+ /** RFC 3339 / ISO-8601 timestamp, e.g. `2026-06-12T18:04:11Z`. */
49
+ type IsoTimestamp = string;
50
+ /**
51
+ * A capability scope. The server is the source of truth (counter + revocation); a token carries
52
+ * its caveats but cannot exceed them (§5).
53
+ *
54
+ * The `mailbox:*` scope strings are the live wire contract (stored in issued keys'
55
+ * caveats) and are NOT renamed despite the public "inbox" product naming — renaming
56
+ * them would invalidate every key already minted. `domain:manage` gates the domains
57
+ * plane; `domain:purchase` is additionally required (and is opt-in, default-off) to
58
+ * buy a new domain (`POST /v1/domains` with `mode: "purchased"`). `review:act` gates
59
+ * the BYO reviewer decision plane.
60
+ */
61
+ type Scope = "mailbox:create" | "mailbox:read" | "mailbox:send" | "mailbox:quota" | "mailbox:delete" | "webhook:write" | "domain:manage" | "domain:purchase" | "review:act";
62
+ /** How a Extrovert domain was onboarded (§7). */
63
+ type OnboardingMode = "shared" | "purchased" | "ns_delegated" | "manual";
64
+ /** Lifecycle status of an agent principal. */
65
+ type AgentStatus = "active" | "disabled";
66
+ /** Lifecycle status of an inbox. */
67
+ type InboxStatus = "provisioning" | "live" | "disabled" | "deleted";
68
+ /** Direction of a message relative to the inbox that owns it. */
69
+ type MessageDirection = "inbound" | "outbound";
70
+ /**
71
+ * Request body for `POST /v1/enroll`. An agent redeems a `pk_enroll_...` token to mint a scoped
72
+ * agent key. Idempotent on `agent_handle` (à la AgentMail's `client_id`).
73
+ */
74
+ interface EnrollRequest {
75
+ /**
76
+ * The raw enrollment token, format `pk_enroll_<id>_<secret>`. Shown once at issue
77
+ * time. REQUIRED — this is the wire field the server reads (`json:"token"`); the
78
+ * request is serialized verbatim, so the field name must match the contract.
79
+ */
80
+ token: string;
81
+ /**
82
+ * Stable client-chosen handle for this agent. Redeeming twice with the same handle returns the
83
+ * same agent rather than minting a new one.
84
+ */
85
+ agent_handle: string;
86
+ /** Optional human-readable label for the minted agent. */
87
+ agent_name?: string;
88
+ /**
89
+ * Optional idempotency key (sent as the `Idempotency-Key` header). A retry with
90
+ * the same key replays the original enrollment response instead of re-minting.
91
+ */
92
+ client_id?: string;
93
+ }
94
+ /**
95
+ * Response from `POST /v1/enroll`. The `agent_key` is the short-lived, per-agent scoped key the
96
+ * agent uses for all subsequent calls — never an org-wide key (§5, §14).
97
+ */
98
+ interface EnrollResponse {
99
+ /** The minted agent principal id, e.g. `agt_7Hq2...`. */
100
+ agent_id: string;
101
+ /**
102
+ * The scoped agent key, format `pk_agent_<id>_<secret>`. Returned once. Treat as a secret and
103
+ * pass as the `Authorization: Bearer` credential on subsequent requests.
104
+ */
105
+ agent_key: string;
106
+ /** Scopes granted to this key (a subset of the enrollment token's scopes). */
107
+ scopes: Scope[];
108
+ /**
109
+ * The fixed org the minted key is bound to (the token's resolved org). The agent
110
+ * cannot change it; it is the canonical org for every subsequent call.
111
+ */
112
+ org_id?: string;
113
+ /**
114
+ * The fixed project the minted key is bound to (the token's resolved project). The
115
+ * agent cannot change it — there is no mutable project selector for a scoped key.
116
+ */
117
+ project_id?: string;
118
+ }
119
+ /** A Extrovert agent principal (read shape). */
120
+ interface Agent {
121
+ id: string;
122
+ name: string | null;
123
+ status: AgentStatus;
124
+ scopes: Scope[];
125
+ created_at: IsoTimestamp;
126
+ metadata: Record<string, string>;
127
+ }
128
+ /**
129
+ * One arbitrary metadata value stored on an inbox. The wire allows string, number,
130
+ * or boolean (nested objects and arrays are rejected). In a PATCH (update) body a
131
+ * value of `null` for a key DELETES that key (the merge/null-delete semantics —
132
+ * see {@link UpdateInboxRequest.metadata}); a read shape ({@link Inbox.metadata})
133
+ * never contains `null`.
134
+ */
135
+ type InboxMetadataValue = string | number | boolean;
136
+ /**
137
+ * The arbitrary key-value metadata object an agent attaches to an inbox (AgentMail
138
+ * parity). Caps: ≤256 keys, ≤256 chars per key, ≤256 chars per string value. The
139
+ * read shape is always an object (`{}` when empty, never null); the PATCH shape
140
+ * additionally allows per-key `null` to delete a key.
141
+ */
142
+ type InboxMetadata = Record<string, InboxMetadataValue>;
143
+ /**
144
+ * The metadata patch shape (create/update bodies): each value may be a
145
+ * string/number/boolean to set it, or `null` to delete that key on a PATCH.
146
+ */
147
+ type InboxMetadataPatch = Record<string, InboxMetadataValue | null>;
148
+ /**
149
+ * Request body for `POST /v1/inboxes`. All fields optional: the default path mints an address on a
150
+ * pre-warmed, pre-ACS-verified shared subdomain of `smtp.extrovert.dev`, so creation is instant.
151
+ */
152
+ interface CreateInboxRequest {
153
+ /**
154
+ * Desired local part (before the `@`). If omitted, the server generates a random handle.
155
+ * Example: `agent7` -> `agent7@smtp.extrovert.dev`.
156
+ */
157
+ username?: string;
158
+ /**
159
+ * Domain to mint on. Must be an org domain the calling key is scoped to. If omitted, the default
160
+ * shared subdomain is used (zero-config path).
161
+ */
162
+ domain?: string;
163
+ /** Human-readable display name used in the `From:` header on sends. */
164
+ display_name?: string;
165
+ /**
166
+ * Idempotency handle. Re-creating with the same `client_id` returns the existing inbox rather
167
+ * than minting a duplicate.
168
+ */
169
+ client_id?: string;
170
+ /**
171
+ * Optional inbound webhook to register at create time (HMAC-signed
172
+ * `message.received`). This is the wire field the server reads (`json:"webhook_url"`);
173
+ * the create body is serialized verbatim, so the name must match the contract.
174
+ */
175
+ webhook_url?: string;
176
+ /**
177
+ * Optional arbitrary key-value metadata to store on the inbox (AgentMail parity).
178
+ * Values may be a string, number, or boolean; nested objects/arrays are rejected.
179
+ * Caps: ≤256 keys, ≤256 chars per key, ≤256 chars per string value. Echoed back on
180
+ * the create response (and replayed verbatim on an idempotent `client_id` retry).
181
+ */
182
+ metadata?: InboxMetadata;
183
+ /**
184
+ * Optional assertion that must match the key's bound project — NEVER a selector.
185
+ * A mismatch is a 403. The inbox is always created in the key's stored project;
186
+ * the assertion only lets a caller defend against a misrouted key.
187
+ */
188
+ project_id?: string;
189
+ /**
190
+ * Whether to return the inbox credentials (IMAP/SMTP password) in the response. Defaults to
191
+ * false; credentials are sealed at rest and only surfaced on explicit request.
192
+ */
193
+ return_credentials?: boolean;
194
+ }
195
+ /**
196
+ * Request body for `PATCH /v1/inboxes/{addr}`. Cheap, in-place inbox settings an
197
+ * owning agent may change without delete+recreate. Every field is optional; an
198
+ * omitted field leaves the stored value untouched (PATCH semantics).
199
+ */
200
+ interface UpdateInboxRequest {
201
+ /**
202
+ * New sender display / `From` name, propagated to the inbox and the authenticated sender.
203
+ * An empty string falls back to the address local-part at the mail layers.
204
+ */
205
+ display_name?: string;
206
+ /** Replace the inbox's inbound webhook target. An empty string clears it. */
207
+ webhook_url?: string;
208
+ /**
209
+ * Set this inbox's effective rolling-24-hour recipient cap. Must be an integer
210
+ * from 1 through 10,000. Updating this field requires the opt-in
211
+ * `mailbox:quota` scope; the other mutable fields do not.
212
+ */
213
+ daily_send_limit?: number;
214
+ /**
215
+ * Patch the inbox's arbitrary metadata (AgentMail parity). Shallow merge:
216
+ * - omitting `metadata` entirely leaves the stored metadata unchanged;
217
+ * - an object MERGES into the existing metadata (set/overwrite the given keys);
218
+ * - a key whose value is `null` DELETES that key;
219
+ * - the top-level value `null` CLEARS all metadata (the response then carries `{}`).
220
+ *
221
+ * Values may be a string, number, or boolean; nested objects/arrays are rejected;
222
+ * the same ≤256 key/length caps as create apply.
223
+ */
224
+ metadata?: InboxMetadataPatch | null;
225
+ /**
226
+ * Optional assertion that must match the key's bound project — NEVER a selector.
227
+ * A mismatch is a 403.
228
+ */
229
+ project_id?: string;
230
+ }
231
+ /**
232
+ * IMAP/SMTP connection config + login for an inbox, only present when
233
+ * `return_credentials` was requested. (The IMAP/SMTP host/port/password are mail
234
+ * protocol internals — the "credentials" of the underlying mailbox.)
235
+ */
236
+ interface InboxCredentials {
237
+ imap_host: string;
238
+ imap_port: number;
239
+ smtp_host: string;
240
+ smtp_port: number;
241
+ username: string;
242
+ /** Plaintext password — shown once, never re-retrievable. Treat as a secret. */
243
+ password: string;
244
+ }
245
+ /** A provisioned inbox (read shape). */
246
+ interface Inbox {
247
+ /**
248
+ * Resource type discriminator. Always `"inbox"` on the redesigned surface (RFC D9:
249
+ * every resource carries `object` + `org_id` + `project_id` + timestamps). Optional
250
+ * for back-compat with legacy/mock shapes that omit it.
251
+ */
252
+ object?: "inbox";
253
+ /**
254
+ * The canonical OPAQUE inbox id and path key (`/v1/inboxes/{inbox_id}` /
255
+ * `/v1/projects/{project_id}/inboxes/{inbox_id}`). Treat it as opaque — do not parse
256
+ * the `pmbx_` prefix.
257
+ */
258
+ id: string;
259
+ /**
260
+ * The fixed org this inbox belongs to (RFC D9). Optional for legacy/mock shapes.
261
+ */
262
+ org_id?: string;
263
+ /**
264
+ * The project this inbox belongs to (RFC D9) — the partition key. Optional for
265
+ * legacy/mock shapes.
266
+ */
267
+ project_id?: string;
268
+ /** Full address, e.g. `agent7@smtp.extrovert.dev`. A within-project email alias for {@link Inbox.id}. */
269
+ address: string;
270
+ username: string;
271
+ domain: string;
272
+ display_name: string | null;
273
+ status: InboxStatus;
274
+ /** Onboarding mode of the domain this inbox lives on. */
275
+ onboarding_mode: OnboardingMode;
276
+ /** Agent that owns this inbox, if minted by an agent key. */
277
+ agent_id: string | null;
278
+ /** Effective enforced rolling-24-hour recipient cap for this inbox. */
279
+ daily_send_limit: number;
280
+ /**
281
+ * Inbound webhook registered for this inbox, if any. The wire field is
282
+ * `webhook_url` (the server returns `webhook_url`, never `inbound_webhook_url`);
283
+ * absent/omitted when no webhook is set.
284
+ */
285
+ webhook_url?: string | null;
286
+ /**
287
+ * Arbitrary key-value metadata stored on the inbox (AgentMail parity). Always an
288
+ * object — `{}` when none is set, never null. Values are string, number, or
289
+ * boolean. Project-scoped: an agent key only reads/mutates metadata for inboxes in
290
+ * its bound project.
291
+ */
292
+ metadata: InboxMetadata;
293
+ created_at: IsoTimestamp;
294
+ /**
295
+ * The RESOLVED {@link ReviewPolicy} for THIS inbox: the per-inbox override,
296
+ * else the account default, else the `require_review` floor.
297
+ *
298
+ * Present on the SINGLE-inbox read only; the list response omits it, because
299
+ * populating it per row would be one settings read per row for a value that is
300
+ * identical across every inbox in the org.
301
+ */
302
+ effective_review_policy?: ReviewPolicy;
303
+ /** Present only on the create response when `return_credentials` was requested. */
304
+ credentials?: InboxCredentials;
305
+ }
306
+ /** Query params for `GET /v1/inboxes`. */
307
+ interface ListInboxesParams {
308
+ /** Filter to a single domain. */
309
+ domain?: string;
310
+ status?: InboxStatus;
311
+ /** Max items to return (server caps this). */
312
+ limit?: number;
313
+ /** Opaque cursor from a previous page's `next_cursor`. */
314
+ cursor?: string;
315
+ }
316
+ /**
317
+ * Query params for the canonical project-prefixed inbox list
318
+ * (`GET /v1/projects/{project_id}/inboxes`, the `x.projects.inboxes.list` chain).
319
+ * Returns the {@link List} envelope with opaque cursors. `include` expands the
320
+ * per-resource relation allowlist (`agent`, `domain`; depth ≤ 2).
321
+ */
322
+ interface ProjectInboxListParams {
323
+ /** Page size (server clamps to 1–100; default 50). */
324
+ limit?: number;
325
+ /** Opaque cursor from a prior page's `next_cursor`. */
326
+ cursor?: string;
327
+ /** Relation expansions (`["agent","domain"]`) serialized to `?include=agent,domain`. */
328
+ include?: InboxInclude[];
329
+ }
330
+ /** Query params for the project-prefixed single-inbox read (`include=` expansion). */
331
+ interface GetInboxParams {
332
+ /** Relation expansions (`["agent","domain"]`). */
333
+ include?: InboxInclude[];
334
+ }
335
+ /**
336
+ * A page of results. This is the canonical envelope shared by Go + MCP + SDK:
337
+ * `items` holds the page, `total` the full match count, and `next_cursor` the
338
+ * opaque cursor (an offset) to fetch the next page (absent on the last page).
339
+ */
340
+ interface Page<T> {
341
+ items: T[];
342
+ /** Total count when the server can compute it cheaply. */
343
+ total: number;
344
+ /** Cursor to pass as `cursor` to fetch the next page; absent when exhausted. */
345
+ next_cursor?: string;
346
+ }
347
+ /** An email address with an optional display name. */
348
+ interface EmailAddress {
349
+ email: string;
350
+ name?: string | null;
351
+ }
352
+ /**
353
+ * A message attachment (metadata; bytes fetched separately by id). Mirrors the
354
+ * canonical Go wire shape (`attachmentResponse`): `id` is the opaque attachment
355
+ * id addressing one MIME part of the message; `size` is the decoded byte length.
356
+ */
357
+ interface Attachment {
358
+ id: string;
359
+ filename: string;
360
+ content_type: string;
361
+ /** Decoded byte length of the attachment. */
362
+ size: number;
363
+ }
364
+ /**
365
+ * An outbound attachment on send / reply. `content_base64` is the standard
366
+ * base64 encoding of the file bytes. Mirrors the Go `attachmentRequest`.
367
+ */
368
+ interface AttachmentInput {
369
+ filename: string;
370
+ content_type: string;
371
+ /** Standard base64 of the raw file bytes. */
372
+ content_base64: string;
373
+ }
374
+ /**
375
+ * A stored message in an inbox (read shape). Mirrors the canonical Go wire shape
376
+ * (`messageResponse`): `id` is the opaque, inbox-resolvable id; `inbox` is the
377
+ * owning address; `seen` is the native IMAP \Seen read state (Extrovert has no
378
+ * Gmail-style labels — read/unread is the \Seen flag); `folder` is the IMAP
379
+ * mailbox; `date` is the raw `Date` header.
380
+ */
381
+ interface Message {
382
+ id: string;
383
+ thread_id: string;
384
+ /** Owning inbox address. */
385
+ inbox: string;
386
+ direction: MessageDirection;
387
+ from: EmailAddress;
388
+ to: EmailAddress[];
389
+ cc?: EmailAddress[];
390
+ subject: string;
391
+ /** Decoded text/plain MIME alternative; null when absent and never derived from HTML. */
392
+ text: string | null;
393
+ /** Decoded text/html MIME alternative; null when absent and never synthesized from text. */
394
+ html?: string | null;
395
+ /** Best-effort derivative of text; null when unavailable and never authoritative. */
396
+ extracted_text?: string | null;
397
+ /** Best-effort derivative of html; null when unavailable and never authoritative. */
398
+ extracted_html?: string | null;
399
+ /** RFC 5322 `Message-ID` header value. */
400
+ message_id: string;
401
+ /** IMAP folder the message lives in (e.g. `INBOX`, `Junk`). */
402
+ folder?: string;
403
+ /** Whether the message has been read (native IMAP \Seen flag). */
404
+ seen: boolean;
405
+ /** Raw `Date` header / received timestamp. */
406
+ date: string;
407
+ }
408
+ /**
409
+ * Query params for `GET /v1/inboxes/{addr}/messages`. The server applies
410
+ * exact-field substring filters (from/to/subject) and an `unread` filter
411
+ * (native \Seen), and pages with limit + offset (cursor is an offset).
412
+ */
413
+ interface ListMessagesParams {
414
+ /** Substring match on subject. */
415
+ subject?: string;
416
+ /** Substring match on sender address. */
417
+ from?: string;
418
+ /** Substring match on a recipient address. */
419
+ to?: string;
420
+ /** Only return unread messages (\Seen flag clear). */
421
+ unread?: boolean;
422
+ limit?: number;
423
+ /** Number of messages to skip (paging). */
424
+ offset?: number;
425
+ /** Opaque cursor from a previous page's `next_cursor` (an offset). */
426
+ cursor?: string;
427
+ }
428
+ /** Query params for `GET /v1/inboxes/{addr}/messages/search`. */
429
+ interface SearchMessagesParams {
430
+ /** Full-text query (matched against from/subject/body via IMAP SEARCH). */
431
+ q: string;
432
+ limit?: number;
433
+ offset?: number;
434
+ cursor?: string;
435
+ }
436
+ /** Request body for `PATCH /v1/inboxes/{addr}/messages/{id}` — read-state toggle. */
437
+ interface MarkReadRequest {
438
+ /** true to set the \Seen flag (read), false to clear it (unread). */
439
+ read: boolean;
440
+ }
441
+ /** A folder a batch update may move messages to. */
442
+ type MailFolder = "INBOX" | "Sent" | "Trash" | "Junk" | "Archive";
443
+ /**
444
+ * Request body for `PATCH /v1/inboxes/{addr}/messages/batch`: apply a read-state
445
+ * toggle and/or a folder move to a list of message ids that all belong to the
446
+ * inbox. At least one of `read` / `folder` must be set.
447
+ */
448
+ interface BatchUpdateMessagesRequest {
449
+ /** Opaque message ids (msg_…), all owned by the inbox. Max 200. */
450
+ ids: string[];
451
+ /** When set, set (true) or clear (false) the \Seen flag on each id. */
452
+ read?: boolean;
453
+ /** When set, move each message to this folder. */
454
+ folder?: MailFolder;
455
+ }
456
+ /** Per-id outcome of a batch update: `updated` succeeded, `failed` were skipped. */
457
+ interface BatchUpdateResult {
458
+ updated: string[];
459
+ failed: string[];
460
+ }
461
+ /**
462
+ * Outcome of a message or thread delete. `expunged` is true when removed
463
+ * permanently, false when moved to Trash; `count` is the number of messages
464
+ * affected (1 for a message, the thread size for a thread).
465
+ */
466
+ interface DeleteResult {
467
+ id: string;
468
+ deleted: true;
469
+ expunged: boolean;
470
+ count: number;
471
+ }
472
+ /** Request body for `POST /v1/inboxes/{addr}/send`. */
473
+ interface SendRequest {
474
+ /** A single address or a list; the SDK normalizes it to an array on the wire. */
475
+ to: string | string[];
476
+ subject: string;
477
+ /**
478
+ * The plain-text body. This is the CANONICAL wire name (matching
479
+ * {@link ReplyRequest.text}, {@link ForwardRequest.text} and
480
+ * {@link Message.text}); the server also accepts a deprecated `body` alias for
481
+ * already-deployed callers, which the SDK never emits. At least one of `text` /
482
+ * `html` is required.
483
+ */
484
+ text?: string;
485
+ html?: string;
486
+ /** A single address or a list; normalized to an array on the wire. */
487
+ cc?: string | string[];
488
+ /** A single address or a list; normalized to an array on the wire. */
489
+ bcc?: string | string[];
490
+ /** Override the `Reply-To` header. */
491
+ reply_to?: string;
492
+ /**
493
+ * Idempotency key — a replay with the same key returns the first response
494
+ * instead of sending a second message.
495
+ *
496
+ * Sent as the `Idempotency-Key` HEADER and STRIPPED from the JSON body: the
497
+ * server hashes the raw body to detect a key reused with different content, so
498
+ * a key that rode along inside the body would be part of its own replay hash.
499
+ */
500
+ idempotency_key?: string;
501
+ /** Custom headers to attach (e.g. `List-Unsubscribe`). */
502
+ headers?: Record<string, string>;
503
+ /** Files to attach (base64). Emitted as a multipart/mixed message. */
504
+ attachments?: AttachmentInput[];
505
+ /**
506
+ * Review Loop (HITL): `review` (default per policy) routes the message into the
507
+ * human-review queue; `direct` requests an immediate send. The account/inbox
508
+ * review policy may downgrade `direct` to `review`. Setting any of
509
+ * mode/intent/category_id opts the send into the Review Loop.
510
+ */
511
+ mode?: ReviewMode;
512
+ /** Intent for the human reviewer. Required when the resolved mode is review (D3). */
513
+ intent?: ReviewIntent;
514
+ /** Opaque category id (cat_…) matched from the registry; never a name. */
515
+ category_id?: string;
516
+ /**
517
+ * Agent-supplied confidence (0..1) in the category match. Feeds the submit-time
518
+ * min_confidence auto-send gate ONLY; the server never scores ($0 LLM). Below the
519
+ * effective threshold (or omitted when a threshold is set) the would-be auto-send
520
+ * routes to needs_review with gate_outcome `held:low_confidence`.
521
+ */
522
+ category_confidence?: number;
523
+ }
524
+ /**
525
+ * Request body for the canonical thread-aware reply,
526
+ * `POST /v1/inboxes/{addr}/reply`. Exactly one of `thread_id` / `message_id`
527
+ * selects the parent; the server derives `to` (original participants), the
528
+ * `Re:`-prefixed subject, and the `In-Reply-To` / `References` headers — you do
529
+ * NOT pass `to`. Set `reply_all` to reply to every thread recipient.
530
+ */
531
+ interface ReplyRequest {
532
+ /** Reply to the latest message in this thread. One of thread_id / message_id. */
533
+ thread_id?: string;
534
+ /** Reply to this specific message. One of thread_id / message_id. */
535
+ message_id?: string;
536
+ /** At least one of `text` / `html` is required. */
537
+ text?: string;
538
+ html?: string;
539
+ cc?: string | string[];
540
+ bcc?: string | string[];
541
+ /** Override the `Reply-To` header. */
542
+ reply_to?: string;
543
+ /** Reply to all thread recipients, not just the original sender. */
544
+ reply_all?: boolean;
545
+ /** See {@link SendRequest.idempotency_key} — sent as a header, never in the body. */
546
+ idempotency_key?: string;
547
+ headers?: Record<string, string>;
548
+ /** Files to attach (base64). Emitted as a multipart/mixed message. */
549
+ attachments?: AttachmentInput[];
550
+ /** Review Loop assertion (see {@link SendRequest.mode}). */
551
+ mode?: ReviewMode;
552
+ /** Intent for the human reviewer. Required when the resolved mode is review (D3). */
553
+ intent?: ReviewIntent;
554
+ /** Opaque category id (cat_…) matched from the registry. */
555
+ category_id?: string;
556
+ /** Agent-supplied confidence (0..1); see {@link SendRequest.category_confidence}. */
557
+ category_confidence?: number;
558
+ }
559
+ /**
560
+ * Request body for `POST /v1/inboxes/{addr}/messages/{id}/forward`. Re-sends the
561
+ * referenced message to new recipients, preserving the original content.
562
+ *
563
+ * A forward is governed by the SAME review policy as a send, and for a stronger
564
+ * reason: it is an outbound message to arbitrary NEW recipients that quotes an
565
+ * entire inbound thread. Leaving it outside the policy would have made forward
566
+ * the documented bypass — and a worse one than a bare send, because it
567
+ * exfiltrates a received conversation.
568
+ */
569
+ interface ForwardRequest {
570
+ /** A single address or a list; normalized to an array on the wire. */
571
+ to: string | string[];
572
+ /** Additional recipients on the forward, screened by the same pre-flight as `to`. */
573
+ cc?: string | string[];
574
+ /** Blind recipients on the forward. Never rendered as a header. */
575
+ bcc?: string | string[];
576
+ /** Optional note prepended to the forwarded content. */
577
+ text?: string;
578
+ /**
579
+ * Accepted and IGNORED by the server. The forwarded content is a plain-text
580
+ * quote of the parent; emitting an HTML alternative would show HTML-capable
581
+ * clients the note WITHOUT the forwarded thread.
582
+ */
583
+ html?: string;
584
+ /** Review Loop assertion (see {@link SendRequest.mode}). */
585
+ mode?: ReviewMode;
586
+ /** Intent for the human reviewer. Required when the resolved mode is review (D3). */
587
+ intent?: ReviewIntent;
588
+ /** Opaque category id (cat_…) matched from the registry. */
589
+ category_id?: string;
590
+ /** Agent-supplied confidence (0..1); see {@link SendRequest.category_confidence}. */
591
+ category_confidence?: number;
592
+ /** See {@link SendRequest.idempotency_key} — sent as a header, never in the body. */
593
+ idempotency_key?: string;
594
+ }
595
+ /**
596
+ * The LEGACY immediate-send 202 body, returned when a caller mentioned nothing
597
+ * about the review loop and the resolved policy permitted a direct send.
598
+ *
599
+ * Its shape differs per verb, which is why almost every field is optional and
600
+ * this type is NOT the whole story (see {@link SendOutcome}):
601
+ *
602
+ * - `send` → `{status:"sent", message_id, review_id}` — **no `thread_id`**.
603
+ * - `reply`/`forward` → `{message_id, thread_id, review_id}` — no `status`.
604
+ *
605
+ * `thread_id` was declared REQUIRED here for a long time while the send path
606
+ * never returned one, so `res.thread_id` typechecked and was `undefined` at
607
+ * runtime. It is optional now because that is the truth.
608
+ */
609
+ interface SendResult {
610
+ /**
611
+ * Discriminant hole. This legacy body carries NO `kind` field, unlike the two
612
+ * review-loop bodies it shares a union with; declaring it as absent is what
613
+ * lets `if (res.kind === "queued_for_review")` narrow a {@link SendOutcome}.
614
+ */
615
+ kind?: undefined;
616
+ /** Extrovert message id of the sent outbound message. */
617
+ message_id: string;
618
+ /** Thread id — present on reply/forward; ABSENT on the direct-send response. */
619
+ thread_id?: string;
620
+ /**
621
+ * Opaque review id (rr_…) of the review row that governed this send. Every
622
+ * agent-plane send now creates one, so an agent that crashed after issuing the
623
+ * request can still call `reviews.get(id)` and read `closed` / `sent_message_id`
624
+ * instead of guessing whether the message went out.
625
+ */
626
+ review_id?: string;
627
+ /** RFC 5322 `Message-ID` header assigned by the sender, when known. */
628
+ message_id_header?: string;
629
+ status?: "queued" | "sent";
630
+ created_at?: IsoTimestamp;
631
+ }
632
+ /**
633
+ * Every shape `inbox.send()` / `.reply()` / `.forward()` / `.submitForReview()`
634
+ * can return, discriminated by `kind`.
635
+ *
636
+ * There are three, because the resolved review policy — not the caller — decides
637
+ * what happens to an outbound message:
638
+ *
639
+ * - {@link QueuedForReviewResult} (`kind:"queued_for_review"`, 202) — parked for
640
+ * a human. **Nothing has been delivered yet.** Monitor
641
+ * `reviewEvents.wait({review_id})` until a `sent` or `send_failed` event
642
+ * arrives.
643
+ * - {@link SentResult} (`kind:"sent"`, 200) — delivered immediately, returned to
644
+ * callers that opted into the review loop by passing mode/intent/category_id.
645
+ * - {@link SendResult} (no `kind`, 202) — the legacy immediate-send body for a
646
+ * caller that mentioned none of those fields.
647
+ *
648
+ * Under the default `require_review` policy a send WITHOUT an `intent` does not
649
+ * return any of these: it raises `IntentRequiredError` (422) and nothing is sent
650
+ * or queued. Use {@link isQueuedForReview} / {@link sentMessageIdOf} from
651
+ * `send-result.js` rather than reaching for a field that may not be there.
652
+ */
653
+ type SendOutcome = SendResult | SentResult | QueuedForReviewResult;
654
+ /** Per-send agent assertion (D3/D6). The resolved policy may downgrade `direct`. */
655
+ type ReviewMode = "review" | "direct";
656
+ /**
657
+ * The account/inbox review policy — the AUTHORITY on what happens to an outbound
658
+ * message. There is no way for a caller to opt out of it.
659
+ *
660
+ * - `require_review` — the default for every account. A send WITHOUT an `intent`
661
+ * is rejected 422 `intent_required` (nothing sent, nothing queued); a send
662
+ * WITH one is queued for a human (202 `queued_for_review`).
663
+ * - `allow_direct` — a bare send (no mode/intent/category_id) is delivered
664
+ * immediately. Supplying an intent, or `mode: "review"`, still queues it.
665
+ * - `auto_send_graduated` — a categorized message that clears the graduation
666
+ * gates auto-sends; everything else is queued.
667
+ *
668
+ * Read {@link Inbox.effective_review_policy} once before your first send rather
669
+ * than learning the policy by being refused.
670
+ */
671
+ type ReviewPolicy = "require_review" | "allow_direct" | "auto_send_graduated";
672
+ /** Review-request state machine (spec §3.1). */
673
+ type ReviewState = "needs_review" | "in_review" | "chatting" | "stale" | "approved" | "sent" | "auto_sent" | "rejected" | "stalled" | "cancelled" | "failed";
674
+ /** The agent's "for the human reviewer" intent (spec §11). */
675
+ interface ReviewIntent {
676
+ summary: string;
677
+ meta?: {
678
+ goal?: string;
679
+ recipient?: string;
680
+ prior_touches?: number;
681
+ urgency?: string;
682
+ };
683
+ }
684
+ /** A review request (rr_…) — the pre-send record under the Review Loop. */
685
+ interface Review {
686
+ id: string;
687
+ state: ReviewState;
688
+ mode: ReviewMode;
689
+ effective_mode: ReviewMode;
690
+ kind: "send" | "reply" | "forward";
691
+ from_address: string;
692
+ agent_id: string;
693
+ category_id?: string;
694
+ intent_summary: string;
695
+ intent_meta?: Record<string, unknown>;
696
+ revision: number;
697
+ version: number;
698
+ proposed_subject: string;
699
+ proposed_body_text: string;
700
+ proposed_body_html?: string;
701
+ proposed_to: string[];
702
+ proposed_cc?: string[];
703
+ proposed_bcc?: string[];
704
+ sent_subject?: string;
705
+ sent_body_text?: string;
706
+ diff_unified?: string;
707
+ sent_message_id?: string;
708
+ gate_outcome?: string;
709
+ stale_reason?: string;
710
+ decision_feedback?: string;
711
+ /**
712
+ * The DEFINITIVE per-review "am I done?" answer, and the poll-side companion to
713
+ * the terminal review events. True for `sent`, `auto_sent`, `cancelled` **and
714
+ * `failed`**.
715
+ *
716
+ * `failed` is included deliberately even though it is not in the formal
717
+ * terminal set: nothing in the product can move a failed review — the console
718
+ * cannot re-approve it — so a flag that said `false` there would invite an
719
+ * agent to wait forever on a row nobody will ever touch.
720
+ *
721
+ * An agent that lost its event cursor (a crash, a fresh process) reads this
722
+ * instead of guessing from the state string.
723
+ */
724
+ closed?: boolean;
725
+ /**
726
+ * The vendor-scrubbed delivery failure, present on a failed review. Until this
727
+ * field existed an agent could learn THAT its message failed and never WHY.
728
+ */
729
+ send_error?: string;
730
+ /**
731
+ * How the message was released, once sent: `human_reviewed`,
732
+ * `reviewer_approved`, `graduated_auto` or `agent_direct` — without a turns
733
+ * fetch.
734
+ */
735
+ send_path?: string;
736
+ created_at: IsoTimestamp;
737
+ updated_at: IsoTimestamp;
738
+ decided_at?: IsoTimestamp;
739
+ sent_at?: IsoTimestamp;
740
+ }
741
+ /** One immutable turn in a review's append-only thread (turn_…). */
742
+ interface ReviewTurn {
743
+ id: string;
744
+ seq: number;
745
+ turn_type: string;
746
+ actor_kind: "agent" | "human" | "review_agent" | "system";
747
+ actor_id?: string;
748
+ body?: string;
749
+ revision?: number;
750
+ diff_json?: Record<string, unknown>;
751
+ metadata?: Record<string, unknown>;
752
+ created_at: IsoTimestamp;
753
+ }
754
+ /** Filters for listing review requests (spec §5.2). */
755
+ interface ListReviewsParams {
756
+ state?: ReviewState | ReviewState[];
757
+ category_id?: string;
758
+ inbox?: string;
759
+ limit?: number;
760
+ page?: string;
761
+ }
762
+ /** One human/agent comment in the assembled review feedback (spec §11). */
763
+ interface ReviewFeedbackComment {
764
+ turn_id: string;
765
+ actor_kind: "agent" | "human" | "review_agent" | "system";
766
+ actor_id?: string;
767
+ body: string;
768
+ created_at: IsoTimestamp;
769
+ }
770
+ /**
771
+ * The human's assembled feedback for a review (spec §11), returned by
772
+ * `reviews.feedback(id)`: the unified + structured diff of the human edit, the human
773
+ * comments / rejection feedback, the decision, and the rules born from this review
774
+ * (rule_ ids whose source_review_id is this review). $0 LLM — pure assembly.
775
+ */
776
+ interface ReviewFeedback {
777
+ review_id: string;
778
+ decision: string;
779
+ diff_unified?: string;
780
+ diff_json?: Record<string, unknown>;
781
+ comments: ReviewFeedbackComment[];
782
+ new_rules: string[];
783
+ }
784
+ /** Body for posting a chat turn on a review's thread (spec §5.2; M5). */
785
+ interface PostReviewChatRequest {
786
+ /** The agent's question/comment for the human reviewer. */
787
+ text: string;
788
+ }
789
+ /**
790
+ * Body for posting a new agent draft under a parent_revision CAS (spec §5.2; M5,
791
+ * D17). parent_revision is the PRIMARY CAS — it must equal the draft's current
792
+ * revision, else 409 STALE with NO mutation. version is OPTIONAL belt-and-suspenders.
793
+ */
794
+ interface SubmitRevisionRequest {
795
+ parent_revision: number;
796
+ version?: number;
797
+ subject?: string;
798
+ /**
799
+ * The redrafted plain-text body. Canonical, matching `text` on send / reply /
800
+ * forward — the same concept should not have two names in the one flow an
801
+ * agent runs most.
802
+ */
803
+ text?: string;
804
+ /**
805
+ * @deprecated Permanent alias for {@link SubmitRevisionRequest.text}. Kept
806
+ * forever so existing callers never break. Sending both with DIFFERENT values
807
+ * is rejected 400 `conflicting_alias`; the server decides, so both are
808
+ * forwarded verbatim rather than resolved client-side.
809
+ */
810
+ body?: string;
811
+ html?: string;
812
+ built_at?: IsoTimestamp;
813
+ rules_version_seen?: number;
814
+ /**
815
+ * REPLACES the draft's attachments. Omit the field to leave them untouched;
816
+ * send an empty array to clear them.
817
+ *
818
+ * Without this a redraft could never restore an attachment, so an agent that
819
+ * redrafted after reviewer feedback would ship a message the human reviewed
820
+ * WITH a file and the recipient received without one.
821
+ */
822
+ attachments?: AttachmentInput[];
823
+ /** Stable retry key, sent as Idempotency-Key and never in the JSON body. */
824
+ idempotency_key?: string;
825
+ }
826
+ /**
827
+ * The reason a durable review nudge was enqueued (spec §4.5). The agent branches
828
+ * on it to decide what to do (redraft, learn, re-check a category, …).
829
+ */
830
+ type ReviewEventReason =
831
+ /** Redraft via `submit_revision` (a reviewer rejected, escalated, or swept it). */
832
+ "redraft_requested"
833
+ /** A HUMAN added chat/feedback. Answer it, or redraft. (An agent's own question emits none.) */
834
+ | "feedback_added"
835
+ /** Re-check the draft's category assignment. */
836
+ | "recheck_category"
837
+ /** A rule changed. Re-read the rules, then redraft or `restamp_review`. */
838
+ | "rule_changed"
839
+ /** A newly general rule now applies to your drafts. */
840
+ | "propagate_general_rule"
841
+ /** A human rejected the draft. Learn from the feedback; redraft or stop. */
842
+ | "rejected"
843
+ /** Delivered. `payload` carries `message_id`, `send_path`, `sent_at`. */
844
+ | "sent"
845
+ /**
846
+ * Delivery failed. `payload.error` is the scrubbed reason and
847
+ * `payload.agent_retryable` is `false`: the only edge out of `failed` is
848
+ * cancel, so compose and submit a NEW message rather than retrying this one.
849
+ */
850
+ | "send_failed"
851
+ /** Withdrawn — by you, by a human, or as the close-out of a failed send. */
852
+ | "cancelled"
853
+ /**
854
+ * You were front-run: the review reached a terminal state while you were
855
+ * trying to act on it, so your `submit_revision` / `post_review_chat` /
856
+ * `cancel_review` answered 409 `terminal`. STOP retrying that review.
857
+ */
858
+ | "front_run_next"
859
+ /** RESERVED — never emitted. Terminal success is `sent`. */
860
+ | "approved"
861
+ /** RESERVED — no production producer (the D13 staleness detector is unbuilt). */
862
+ | "staleness";
863
+ /**
864
+ * One durable review nudge (ndg_…) drained from the AUTHORITATIVE liveness queue
865
+ * (spec §11). `seq` is the per-review monotonic ordinal the ack cursor advances
866
+ * against (0 for a broadcast nudge). Opaque typed ids only (D10).
867
+ */
868
+ interface ReviewEvent {
869
+ seq: number;
870
+ id: string;
871
+ reason: ReviewEventReason;
872
+ review_id?: string;
873
+ category_id?: string;
874
+ payload?: Record<string, unknown>;
875
+ created_at: IsoTimestamp;
876
+ }
877
+ /** The agent's per-(agent, review) ack frontier — its strict-FIFO position. */
878
+ interface ReviewEventCursor {
879
+ review_id: string;
880
+ last_acked_seq: number;
881
+ }
882
+ /** Drain result for list/wait — un-acked events in FIFO seq order + cursors. */
883
+ interface ReviewEventsResult {
884
+ events: ReviewEvent[];
885
+ cursors?: ReviewEventCursor[];
886
+ }
887
+ /** Filters for draining review events (spec §5.9 list_review_events). */
888
+ interface ListReviewEventsParams {
889
+ /** Restrict the drain to one review's events (rr_…). */
890
+ review_id?: string;
891
+ /** Max events to return in one drain. */
892
+ limit?: number;
893
+ }
894
+ /**
895
+ * A category (cat_…) in the Review Loop registry (D9/D10). `name` + `description`
896
+ * are skill-style metadata the agent fuzzy-matches against; nothing keys on the
897
+ * name (renames never break a reference). Categories are CUSTOMER-scoped and
898
+ * agent-attributed — the deliberate cross-agent-404 exception. Opaque ids only.
899
+ */
900
+ interface Category {
901
+ id: string;
902
+ name: string;
903
+ description: string;
904
+ scope: "org_shared" | "agent_private";
905
+ state: "supervised" | "auto_notify" | "auto_silent";
906
+ /** Survivor id when this category was merged / soft-deleted (cat_…). */
907
+ merged_into?: string;
908
+ created_by_agent_id?: string;
909
+ author_kind: "agent" | "human";
910
+ rule_high_water: number;
911
+ rules_version: number;
912
+ created_at: IsoTimestamp;
913
+ updated_at: IsoTimestamp;
914
+ }
915
+ /** Browse filter for the category registry (spec §5.5). */
916
+ interface ListCategoriesParams {
917
+ /** Pure lexical substring filter over name+description (every token must match; NO LLM). */
918
+ match?: string;
919
+ }
920
+ /** Propose a new category (spec §5.5; D9). */
921
+ interface ProposeCategoryRequest {
922
+ name: string;
923
+ description?: string;
924
+ /** Defaults to org_shared server-side. */
925
+ scope?: "org_shared" | "agent_private";
926
+ }
927
+ /** Rename / re-describe a category — metadata only (spec §5.5; D10). */
928
+ interface UpdateCategoryRequest {
929
+ name?: string;
930
+ description?: string;
931
+ }
932
+ /**
933
+ * The account-wide default risk dial (Review Loop, D4/D12) — the values a per-
934
+ * category null override inherits. The single user-configurable brand-risk lever.
935
+ */
936
+ interface AccountRiskDial {
937
+ min_confidence: number;
938
+ first_contact_gate: boolean;
939
+ drift_demote_after: number;
940
+ canary_rate: number;
941
+ graduate_min_approvals: number;
942
+ graduate_min_age_hours: number;
943
+ auto_send_cap_per_day: number;
944
+ }
945
+ /** The RESOLVED risk dial for a category: account default with per-category override applied (D12). */
946
+ interface EffectiveRiskDial {
947
+ min_confidence: number;
948
+ first_contact_gate: boolean;
949
+ drift_demote_after: number;
950
+ canary_rate: number;
951
+ graduate_min_approvals: number;
952
+ graduate_min_age_hours: number;
953
+ auto_send_cap_per_day: number;
954
+ }
955
+ /**
956
+ * One category's risk-dial OVERRIDE columns (null = inherit the account default;
957
+ * D12) alongside the resolved effective dial.
958
+ */
959
+ interface CategoryRiskDial {
960
+ category_id: string;
961
+ min_confidence: number | null;
962
+ first_contact_gate: boolean | null;
963
+ drift_demote_after: number | null;
964
+ canary_rate: number | null;
965
+ graduate_min_approvals: number | null;
966
+ graduate_min_age_hours: number | null;
967
+ effective: EffectiveRiskDial;
968
+ }
969
+ /**
970
+ * The effective risk dial (Review Loop, agent plane; D4/D12): the account default
971
+ * plus every category's overrides. Read-only for agents — flipping the dial is a
972
+ * console (human) action (D16).
973
+ */
974
+ interface RiskDial {
975
+ account: AccountRiskDial;
976
+ categories: CategoryRiskDial[];
977
+ }
978
+ /**
979
+ * The graduation gate status toward the NEXT rung (Review Loop, D16): clean approvals
980
+ * (N / needed), category age, the maturity gate (auto_silent precondition), drift vs
981
+ * K, and whether a human graduate would succeed right now. Read-only.
982
+ */
983
+ interface GraduationStatus {
984
+ category_id: string;
985
+ state: "supervised" | "auto_notify" | "auto_silent";
986
+ /** The rung a graduate would move to (empty if none). */
987
+ next_state: string;
988
+ never_graduate: boolean;
989
+ clean_approval_count: number;
990
+ graduate_min_approvals: number;
991
+ approvals_met: boolean;
992
+ age_hours: number;
993
+ graduate_min_age_hours: number;
994
+ age_met: boolean;
995
+ maturity_gate_met: boolean;
996
+ drift_count: number;
997
+ drift_demote_after: number;
998
+ can_graduate: boolean;
999
+ }
1000
+ /** Record a graduation request (spec §5.6; D16/D6). evidence is opaque agent context. */
1001
+ interface ProposeGraduationRequest {
1002
+ evidence?: Record<string, unknown>;
1003
+ }
1004
+ /**
1005
+ * The D19/§8 re-stamp-without-redraft escape valve ($0). The agent asserts it reviewed
1006
+ * the draft against rules `against_version` and no change is needed; the server
1007
+ * advances the draft's composed_* rules-versions WITHOUT a new draft. against_version
1008
+ * must not exceed the category's current rules-version.
1009
+ */
1010
+ interface RestampReviewRequest {
1011
+ /** The category rules-version the agent reviewed against (≤ the current version). */
1012
+ against_version: number;
1013
+ /** Optional: re-stamp the house-style axis to this version (≤ the current version). */
1014
+ house_style_version?: number;
1015
+ /** Stable retry key, sent as Idempotency-Key and never in the JSON body. */
1016
+ idempotency_key?: string;
1017
+ }
1018
+ /** The reviewer's decision verb (BYO review-agent plane; D5/§9). */
1019
+ type ReviewerAction = "approve" | "edit" | "reject" | "escalate";
1020
+ /**
1021
+ * The REVIEWER's read-only decision surface for a review (BYO review-agent plane;
1022
+ * D5/§9), returned by `reviews.decisionContext(id)`: the intent + current draft + the
1023
+ * append-only thread + the two-circuit-breaker budget. `force_to_human` is true when
1024
+ * EITHER breaker has tripped — the reviewer's next reject would be FORCED to the human
1025
+ * regardless of intent (the human is the only terminal authority, D17).
1026
+ */
1027
+ interface ReviewDecisionContext {
1028
+ review: Review;
1029
+ turns: ReviewTurn[];
1030
+ /** Circuit breaker (a): reviewer hand-backs so far. */
1031
+ hop_count: number;
1032
+ /** Circuit breaker (a): the ceiling; at hop_count ≥ max_hops the next action is forced to the human. */
1033
+ max_hops: number;
1034
+ /** Circuit breaker (b): the hard per-review wall-clock deadline (created_at + review_deadline_s). */
1035
+ review_deadline: IsoTimestamp;
1036
+ /** Breaker (b) tripped. */
1037
+ deadline_passed: boolean;
1038
+ /** Breaker (a) tripped. */
1039
+ hops_exhausted: boolean;
1040
+ /** Either breaker tripped: a reject is overridden to a human escalation. */
1041
+ force_to_human: boolean;
1042
+ /** The tripped breaker (max_hops_reached | review_deadline_passed). */
1043
+ force_reason?: string;
1044
+ }
1045
+ /**
1046
+ * Body for a reviewer decision (`reviews.decide(id, req)`; reviewer_decide, D5/§9).
1047
+ * `action` is approve|edit|reject|escalate. `revision`/`version` are the optimistic CAS
1048
+ * — a mismatch is a 409 STALE with NO mutation (the human always wins, D17). subject/
1049
+ * body carry the edited content for the edit action; feedback is the reviewer's note.
1050
+ */
1051
+ interface ReviewerDecisionRequest {
1052
+ action: ReviewerAction;
1053
+ /** The revision you decided against (PRIMARY CAS; 409 STALE on mismatch). */
1054
+ revision: number;
1055
+ /** Optional row-version CAS (defense in depth). */
1056
+ version?: number;
1057
+ /** Edited subject (edit action). */
1058
+ subject?: string;
1059
+ /** Edited body text (edit action). */
1060
+ body?: string;
1061
+ /** Reviewer note (reject: the rule-birth signal; escalate: the human-facing reason). */
1062
+ feedback?: string;
1063
+ }
1064
+ /**
1065
+ * The outcome of a reviewer decision (D5/§9). `kind=sent` when the platform ACS-sent
1066
+ * with the COMPOSER's creds (approve/edit — the reviewer NEVER holds mailbox:send);
1067
+ * `kind=sent_to_human` when the draft returned to the human queue (reject/escalate, or
1068
+ * a reject FORCED to the human by a circuit breaker, with `forced_by_breaker` naming it).
1069
+ */
1070
+ interface ReviewerDecisionResult {
1071
+ kind: "sent" | "sent_to_human";
1072
+ review: Review;
1073
+ sent: boolean;
1074
+ message_id?: string;
1075
+ thread_id?: string;
1076
+ sent_to_human: boolean;
1077
+ forced_by_breaker?: string;
1078
+ }
1079
+ /**
1080
+ * The D19/§8 backlog-reconciliation snapshot for a category (agent-readable, $0-LLM).
1081
+ * Counts the QUEUED drafts that are stale vs current-enough against the current
1082
+ * category rules-version + house-style version (a pure integer compare). Read-only —
1083
+ * the agent READS the picture; the human / hooks TRIGGER the actual sweep.
1084
+ */
1085
+ interface ScanBacklogStatus {
1086
+ category_id: string;
1087
+ state: "supervised" | "auto_notify" | "auto_silent" | "probation";
1088
+ /** Drafts in the human queue (needs_review|in_review|chatting). */
1089
+ queued: number;
1090
+ /** Of the queued, how many are within tolerance of the current rules-version. */
1091
+ current_enough: number;
1092
+ /** Of the queued, how many were composed under older rules and need a redraft. */
1093
+ stale: number;
1094
+ current_category_rules_version: number;
1095
+ current_house_style_version: number;
1096
+ /** How many versions behind current a draft may be and still count current-enough. */
1097
+ staleness_tolerance: number;
1098
+ }
1099
+ /** One queued draft's pacing classification relative to the cursor + window (§8). */
1100
+ interface PacingItem {
1101
+ review_id: string;
1102
+ state: "behind_cursor" | "in_window_fresh" | "in_window_redrafting" | "ahead";
1103
+ }
1104
+ /**
1105
+ * The demand-driven pacing snapshot for a category (agent-readable, $0-LLM — M7 Slice
1106
+ * B/§8): the human review cursor, the effective window/ceiling/interval, the queued
1107
+ * count, and each queued draft's in-window/redrafting/behind-cursor classification.
1108
+ * Read-only; the cursor advances from the human's console approve/reject/edit actions.
1109
+ */
1110
+ interface CategoryPacingState {
1111
+ category_id: string;
1112
+ /** The last queued draft the human acted on (the cursor); omitted when nothing reviewed yet. */
1113
+ cursor_review_id?: string;
1114
+ /** Monotonic count of cursor advances. */
1115
+ cursor_advanced_count: number;
1116
+ /** Effective freshness window (default org_settings.lookahead_window=3). */
1117
+ lookahead_window: number;
1118
+ /** HARD per-nudge fan-out ceiling (default 10) — one nudge can never fan to 500. */
1119
+ rework_batch_max: number;
1120
+ /** Per-agent token-bucket interval that coalesces feedback storms (default 5000). */
1121
+ nudge_min_interval_ms: number;
1122
+ /** Drafts in the human queue (needs_review|in_review|chatting). */
1123
+ queued: number;
1124
+ /** Of the queued, how many sit in the freshness-guaranteed window. */
1125
+ in_window: number;
1126
+ /** Of the in-window, how many are stale and being redrafted (the console shimmer set). */
1127
+ redrafting: number;
1128
+ items: PacingItem[];
1129
+ }
1130
+ /** Long-poll params: like {@link ListReviewEventsParams} plus a wait budget. */
1131
+ interface WaitForReviewEventParams extends ListReviewEventsParams {
1132
+ /** Long-poll budget in seconds (default ~30, capped ~55). */
1133
+ wait_seconds?: number;
1134
+ }
1135
+ /**
1136
+ * The ownership layer of a writing rule (org/project model). `org` = house-style
1137
+ * inherited by every project in the org; `project` = layered on top of the org rules
1138
+ * (the agent-plane default). Project/per-agent rules outrank broader org rules in the
1139
+ * ordered get_rules precedence ladder.
1140
+ */
1141
+ type RuleLayer = "org" | "project";
1142
+ /**
1143
+ * A learned writing rule (rule_…) in the Review Loop (D2/D11). House-style/general
1144
+ * (scope='general', applies across all categories) or category-scoped. Append-only
1145
+ * by supersession: an edit is a new rev (same lineage_id) with the prior superseded.
1146
+ * Read by the agent at compose/redraft time via the ORDERED get_rules ladder; we
1147
+ * never apply it (NO LLM on our side). Opaque ids only (D10).
1148
+ *
1149
+ * Layering (org/project): `rule_layer` says whether the rule is an org-wide
1150
+ * house-style rule (`org`) or a project-layer rule (`project`). `org_id` is always
1151
+ * set; `project_id` is set for a project-layer rule and empty for an org-layer rule.
1152
+ */
1153
+ interface Rule {
1154
+ id: string;
1155
+ lineage_id: string;
1156
+ rev: number;
1157
+ /**
1158
+ * Ownership layer (org/project). `org` = house-style inherited by every project;
1159
+ * `project` = layered on top (the agent-plane default). Optional/additive so older
1160
+ * servers that don't set it still parse.
1161
+ */
1162
+ rule_layer?: RuleLayer;
1163
+ /** The org this rule belongs to. */
1164
+ org_id?: string;
1165
+ /** The project this rule belongs to; empty for an org-layer rule. */
1166
+ project_id?: string;
1167
+ scope: "general" | "category";
1168
+ /** Set iff scope=category (cat_…). */
1169
+ category_id?: string;
1170
+ /** Set for a per-agent override; empty = all org agents. */
1171
+ scope_agent_id?: string;
1172
+ rule_text: string;
1173
+ kind: "soft" | "hard";
1174
+ priority: number;
1175
+ status: "proposed" | "active" | "superseded" | "retired";
1176
+ supersedes_id?: string;
1177
+ author_kind: "agent" | "human";
1178
+ created_at: IsoTimestamp;
1179
+ updated_at: IsoTimestamp;
1180
+ }
1181
+ /** Filter for the ordered get_rules read (spec §5.4; §7). */
1182
+ interface GetRulesParams {
1183
+ /** Category id (cat_…). Empty returns ONLY the house-style/general layer. */
1184
+ category_id?: string;
1185
+ /** Narrow to one layer (general | category). Default returns both. */
1186
+ scope?: "general" | "category";
1187
+ }
1188
+ /**
1189
+ * Save / edit a writing rule (append-only by supersession; spec §5.4; D11).
1190
+ *
1191
+ * Layering (org/project): an agent-plane save is ALWAYS project-layer — the saved
1192
+ * rule's `rule_layer` is `project`, bound to the calling key's project. There is no
1193
+ * settable `rule_layer` here: an agent cannot create org-layer / house-style
1194
+ * (`rule_layer="org"`) rules in v1; authoring org rules is a console/admin action.
1195
+ * (`scope: "general"` still means a house-style rule WITHIN the project layer —
1196
+ * `scope` is the category axis, `rule_layer` is the ownership axis.)
1197
+ */
1198
+ interface SaveRuleRequest {
1199
+ /** Defaults from category_id (general iff empty). */
1200
+ scope?: "general" | "category";
1201
+ /** Category id (cat_…); empty = house-style/general (D2). */
1202
+ category_id?: string;
1203
+ rule_text: string;
1204
+ /** Defaults soft. hard = non-overridable. */
1205
+ kind?: "soft" | "hard";
1206
+ priority?: number;
1207
+ source_review_id?: string;
1208
+ source_turn_id?: string;
1209
+ /** Set to EDIT the prior version (rule_…). */
1210
+ supersedes_id?: string;
1211
+ /** Set for a per-agent override; empty = all org agents. */
1212
+ scope_agent_id?: string;
1213
+ /**
1214
+ * D8 retro-propagation HUMAN OPT-IN (default false). When true, a NEW category rule
1215
+ * that could apply to pending siblings enqueues ONE propagate_general_rule nudge
1216
+ * (siblings + suggested_batch) so the agent redrafts a FEW at a time — never the
1217
+ * whole queue. Set only after the human said "apply to N pending?".
1218
+ */
1219
+ propagate_to_pending?: boolean;
1220
+ /** Override the propagate batch (0 = base 3, bounded by rework_batch_max). */
1221
+ suggested_batch?: number;
1222
+ }
1223
+ /** One append-only rule/category change & undo audit row (udo_…). */
1224
+ interface RuleAuditEntry {
1225
+ id: string;
1226
+ entity_kind: "rule" | "category";
1227
+ entity_id: string;
1228
+ action: "create" | "supersede" | "retire" | "rename" | "redescribe" | "merge" | "restore";
1229
+ actor_kind: "agent" | "human" | "system";
1230
+ actor_id?: string;
1231
+ before_json?: string;
1232
+ after_json?: string;
1233
+ undone: boolean;
1234
+ created_at: IsoTimestamp;
1235
+ }
1236
+ /** Filter for the rule/category change audit read (spec §5.4; D11). */
1237
+ interface GetRuleAuditParams {
1238
+ entity_kind?: "rule" | "category";
1239
+ entity_id?: string;
1240
+ }
1241
+ /** One per-(agent, review) cursor advance for ack_review_event. */
1242
+ interface AckReviewEventEntry {
1243
+ review_id: string;
1244
+ through_seq: number;
1245
+ }
1246
+ /** Ack request: advance per-review cursor(s) and/or mark broadcast nudges done. */
1247
+ interface AckReviewEventRequest {
1248
+ acks?: AckReviewEventEntry[];
1249
+ broadcast_ids?: string[];
1250
+ }
1251
+ /** Ack result: the resulting per-review cursors. */
1252
+ interface AckReviewEventResult {
1253
+ cursors?: ReviewEventCursor[];
1254
+ }
1255
+ /** A Review Loop submit parked for human review (202). */
1256
+ interface QueuedForReviewResult {
1257
+ kind: "queued_for_review";
1258
+ review: {
1259
+ id: string;
1260
+ state: ReviewState;
1261
+ effective_mode?: ReviewMode;
1262
+ };
1263
+ }
1264
+ /** A Review Loop submit sent immediately (200). */
1265
+ interface SentResult {
1266
+ kind: "sent";
1267
+ message: {
1268
+ id: string;
1269
+ thread_id?: string;
1270
+ };
1271
+ /**
1272
+ * The review row that governed this send (ADDITIVE). Present on every send the
1273
+ * service routed, i.e. all of them — it is the handle that makes a post-crash
1274
+ * `reviews.get(id)` possible on the direct path too.
1275
+ */
1276
+ review?: {
1277
+ id: string;
1278
+ state: ReviewState;
1279
+ };
1280
+ }
1281
+ /** The discriminated outcome of a review-mode submit (queued OR sent). */
1282
+ type SubmitForReviewResult = QueuedForReviewResult | SentResult;
1283
+ /**
1284
+ * A conversation thread (read shape). Grouped server-side by RFC 5322
1285
+ * References / In-Reply-To chaining (subject fallback); `id` is stable across
1286
+ * calls. `participants` are display address strings (`Name <email>` or bare).
1287
+ */
1288
+ interface Thread {
1289
+ id: string;
1290
+ /** Owning inbox address. */
1291
+ inbox_id: string;
1292
+ subject: string;
1293
+ /** Distinct participant address strings across the thread. */
1294
+ participants: string[];
1295
+ message_count: number;
1296
+ last_message_at: IsoTimestamp;
1297
+ /** Most-recent-message preview snippet. */
1298
+ snippet: string;
1299
+ }
1300
+ /** A thread plus its messages (oldest-first) — `GET /v1/inboxes/{addr}/threads/{id}`. */
1301
+ interface ThreadDetail extends Thread {
1302
+ messages: Message[];
1303
+ }
1304
+ /** Query params for `GET /v1/inboxes/{addr}/threads`. */
1305
+ interface ListThreadsParams {
1306
+ limit?: number;
1307
+ offset?: number;
1308
+ cursor?: string;
1309
+ }
1310
+ /**
1311
+ * Request for `POST /v1/inboxes/{addr}/wait`. Polls server-side until a matching message arrives
1312
+ * or the timeout elapses, then returns it with an extracted OTP / verification link.
1313
+ */
1314
+ interface WaitForEmailRequest {
1315
+ /** Only match messages from this sender address or domain. */
1316
+ from?: string;
1317
+ /** Only match messages whose subject contains this substring (case-insensitive). */
1318
+ subject?: string;
1319
+ /** Case-sensitive Go RE2 expression over subject/readable body. Prefix `(?i)` for case-insensitive matching. */
1320
+ match?: string;
1321
+ /** Prefer an extracted link containing this substring; this does not filter message matches. */
1322
+ link_hint?: string;
1323
+ /** Max seconds to block before returning a timeout. Server caps this (default 300, cap 600). */
1324
+ timeout_seconds?: number;
1325
+ /**
1326
+ * If true (default), only consider messages that arrive after the request is made, ignoring the
1327
+ * existing inbox contents. Set false to also match an already-delivered message.
1328
+ */
1329
+ since_now?: boolean;
1330
+ }
1331
+ /** Structured extraction returned alongside the matched message. */
1332
+ interface ExtractedCredentials {
1333
+ /** The first OTP-looking code found in the body (e.g. `492013`), or null. */
1334
+ otp: string | null;
1335
+ /** The first verification/magic link found in the body, or null. */
1336
+ link: string | null;
1337
+ }
1338
+ /** Result of `wait_for_email`. `timed_out` distinguishes "no match in time" from a real match. */
1339
+ interface WaitForEmailResult {
1340
+ /** True when the timeout elapsed before a match; in that case `message` is null. */
1341
+ timed_out: boolean;
1342
+ /** The matched message, or null on timeout. */
1343
+ message: Message | null;
1344
+ /** Structured OTP/link extraction from the matched message body. */
1345
+ extracted: ExtractedCredentials;
1346
+ }
1347
+ /** Webhook event types Extrovert emits. */
1348
+ type WebhookEvent = "message.received";
1349
+ /** Request body for `POST /v1/webhooks`. Registers an HMAC-signed, timestamped endpoint. */
1350
+ interface RegisterWebhookRequest {
1351
+ url: string;
1352
+ /** Events to subscribe to. Defaults to `["message.received"]`. */
1353
+ events?: WebhookEvent[];
1354
+ /** Scope the webhook to a single inbox address; omit for all inboxes the key can read. */
1355
+ inbox?: string;
1356
+ /**
1357
+ * Optional idempotency key (sent as the `Idempotency-Key` header). A retry with
1358
+ * the same key replays the original webhook registration instead of duplicating.
1359
+ */
1360
+ client_id?: string;
1361
+ }
1362
+ /**
1363
+ * PATCH body for `PATCH /v1/webhooks/{id}`. Every field is optional; an omitted
1364
+ * field leaves the stored value unchanged (PATCH semantics). The signing secret
1365
+ * and id are immutable.
1366
+ */
1367
+ interface UpdateWebhookRequest {
1368
+ /** Replace the HTTPS delivery endpoint. */
1369
+ url?: string;
1370
+ /** Replace the subscribed event set. */
1371
+ events?: WebhookEvent[];
1372
+ /** Replace the inbox filter; an empty string clears it (covers all owned inboxes). */
1373
+ inbox?: string;
1374
+ /** Enable or disable delivery without deleting the webhook. */
1375
+ active?: boolean;
1376
+ }
1377
+ /** A registered webhook (read shape). The `secret` is returned once at registration. */
1378
+ interface Webhook {
1379
+ id: string;
1380
+ url: string;
1381
+ events: WebhookEvent[];
1382
+ inbox: string | null;
1383
+ /** Agent that owns this webhook. */
1384
+ agent_id?: string;
1385
+ /**
1386
+ * HMAC signing secret, returned once at registration. Used to verify the `X-Extrovert-Signature`
1387
+ * header on inbound deliveries (see {@link verifyWebhookSignature}). Absent on list/get reads.
1388
+ */
1389
+ secret?: string;
1390
+ /** Display prefix of the secret (safe to store), e.g. `whsec_a1b2`. */
1391
+ secret_prefix: string;
1392
+ /** Whether the webhook is active (deliveries are sent). */
1393
+ active: boolean;
1394
+ created_at: IsoTimestamp;
1395
+ }
1396
+ /** Whether a contact-list entry permits (allow) or rejects (block) a match. */
1397
+ type ContactListKind = "allow" | "block";
1398
+ /** Traffic direction a contact-list entry governs. Only `send` is enforced today. */
1399
+ type ContactListDirection = "send" | "receive";
1400
+ /**
1401
+ * Request body for `POST /v1/inboxes/{addr}/lists`. Adds one allow/block entry.
1402
+ * `pattern` is a bare email address (matched in full) or a bare domain (matches
1403
+ * any address in that domain).
1404
+ */
1405
+ interface AddContactListRequest {
1406
+ kind: ContactListKind;
1407
+ /** Defaults to `send` server-side (the only enforced direction today). */
1408
+ direction?: ContactListDirection;
1409
+ pattern: string;
1410
+ }
1411
+ /**
1412
+ * A contact allow/block-list entry (read shape). `inbox` is null when the entry
1413
+ * is account-wide (covers every inbox the agent owns).
1414
+ */
1415
+ interface ContactListEntry {
1416
+ id: string;
1417
+ inbox: string | null;
1418
+ kind: ContactListKind;
1419
+ direction: ContactListDirection;
1420
+ pattern: string;
1421
+ created_at: IsoTimestamp;
1422
+ }
1423
+ /**
1424
+ * The scope a suppression row applies at. The agent plane only ever sees `org`
1425
+ * rows (the caller's OWN org): the reads never surface a platform-`global` or
1426
+ * shared-domain opt-out (non-leakage). The wider values are part of the shape for
1427
+ * forward-compatibility with the admin/operator plane.
1428
+ */
1429
+ type SuppressionScope = "org" | "shared_domain" | "global";
1430
+ /** How a suppression came to exist (which signal created the opt-out row). */
1431
+ type SuppressionSource = "one_click" | "page" | "mailto" | "reply_stop" | "manual" | "complaint" | "escalation";
1432
+ /**
1433
+ * One recipient opt-out row (mirrors the Go `suppressionResponse`). A recipient
1434
+ * with an active (non-`revoked`) row for the sender's org is blocked from receiving
1435
+ * mail; a send to them is rejected with `recipient_suppressed`
1436
+ * ({@link RecipientSuppressedError}). Revoke a row (with a reason) to re-enable
1437
+ * sending to that recipient.
1438
+ */
1439
+ interface SuppressionEntry {
1440
+ id: string;
1441
+ /** The canonicalized (lower-cased, normalized) recipient the row suppresses. */
1442
+ recipient: string;
1443
+ /** The recipient exactly as it was originally addressed, when it differs. */
1444
+ recipient_raw?: string;
1445
+ scope: SuppressionScope;
1446
+ source: SuppressionSource;
1447
+ /** Set when the row is narrowed to one agent (recipient-chosen narrow row). */
1448
+ narrow_agent_id?: string;
1449
+ /** Set when the row is narrowed to one mailbox (recipient-chosen narrow row). */
1450
+ narrow_mailbox?: string;
1451
+ /** The mailbox the opt-out originated from, when known. */
1452
+ origin_mailbox?: string;
1453
+ /** The agent whose send produced the opt-out, when known. */
1454
+ origin_agent_id?: string;
1455
+ /** The message that carried the unsubscribe link/header, when known. */
1456
+ origin_message_id?: string;
1457
+ /** How many times this recipient re-suppressed after a revoke (abuse signal). */
1458
+ reactivation_count: number;
1459
+ created_at: IsoTimestamp;
1460
+ /** Present once the row is revoked. */
1461
+ revoked_at?: IsoTimestamp;
1462
+ /** Who revoked the row (e.g. `agent:<id>`), when revoked. */
1463
+ revoked_by?: string;
1464
+ /** The required, audit-logged reason the row was revoked. */
1465
+ revoke_reason?: string;
1466
+ /** True when the row has been revoked (no longer enforced). */
1467
+ revoked: boolean;
1468
+ }
1469
+ /**
1470
+ * The result of a pre-check (`GET /v1/suppressions?recipient=…`): whether the
1471
+ * caller's OWN org suppresses the recipient, plus the matching org rows. Reflects
1472
+ * only the caller's org state — never a global/shared/cross-tenant opt-out.
1473
+ */
1474
+ interface SuppressionPrecheck {
1475
+ recipient: string;
1476
+ /** True when the caller's org has an active (non-revoked) row for the recipient. */
1477
+ suppressed: boolean;
1478
+ rows: SuppressionEntry[];
1479
+ }
1480
+ /** Filters + paging for `GET /v1/suppressions` (the caller's own org rows). */
1481
+ interface ListSuppressionsParams {
1482
+ /** Narrow to one scope; the agent plane only ever returns `org` rows. */
1483
+ scope?: SuppressionScope;
1484
+ /** Include revoked rows too (default: active rows only). */
1485
+ include_revoked?: boolean;
1486
+ /** Max rows to return. */
1487
+ limit?: number;
1488
+ /** Opaque cursor from a previous page's `next_cursor`. */
1489
+ cursor?: string;
1490
+ }
1491
+ /** One DNS record the customer must set (manual mode) or that we serve (ns_delegated). */
1492
+ interface DomainRecord {
1493
+ name: string;
1494
+ type: string;
1495
+ value: string;
1496
+ /** MX priority, when applicable. */
1497
+ priority?: number | null;
1498
+ ttl: number;
1499
+ }
1500
+ /** Domain visibility for an onboarded domain (org/project model). */
1501
+ type DomainScope = "org" | "project";
1502
+ /**
1503
+ * Request body for `POST /v1/domains`. Onboards/adds a domain for the customer.
1504
+ *
1505
+ * Permissions: every mode needs the `domain:manage` scope (the route gate); `mode:
1506
+ * "purchased"` spends money at the registrar and therefore ADDITIONALLY requires the
1507
+ * explicit, default-off `domain:purchase` scope (and is capped by the org/project
1508
+ * purchased-domain plan limit, enforced before any registrar spend). `manual` and
1509
+ * `ns_delegated` need `domain:manage` only.
1510
+ */
1511
+ interface OnboardDomainRequest {
1512
+ domain: string;
1513
+ /**
1514
+ * Onboarding path. Defaults to `ns_delegated` server-side when omitted. `purchased`
1515
+ * additionally requires the `domain:purchase` scope.
1516
+ */
1517
+ mode?: OnboardingMode;
1518
+ /** A-record IP served at a delegated zone's apex (ns_delegated only). */
1519
+ mail_host_ip?: string;
1520
+ /**
1521
+ * Domain visibility. Defaults to `org` (org-shared, usable by every project in the
1522
+ * org). `project` binds the domain to the key's OWN bound project (never
1523
+ * client-selected) so it is only visible/mintable from that project. A
1524
+ * legacy/unscoped key (no bound project) falls back to `org`.
1525
+ */
1526
+ scope?: DomainScope;
1527
+ /**
1528
+ * Optional assertion that must match the key's bound project — NEVER a selector.
1529
+ * A mismatch is a 403. The binding is always derived from the key.
1530
+ */
1531
+ project_id?: string;
1532
+ }
1533
+ /**
1534
+ * The agent-facing view of one onboarded domain (mirrors the Go `domainResponse`).
1535
+ * `records` (and `delegation_ns` for ns_delegated) are present on get / onboard /
1536
+ * verify and empty on list reads and for shared/purchased modes.
1537
+ */
1538
+ interface Domain {
1539
+ id: string;
1540
+ domain: string;
1541
+ mode: OnboardingMode;
1542
+ verification_status: string;
1543
+ dkim_status: string;
1544
+ shared: boolean;
1545
+ provisioning_phase?: string;
1546
+ provisioning_error?: string;
1547
+ created_at: IsoTimestamp;
1548
+ records?: DomainRecord[];
1549
+ delegation_ns?: DomainRecord[];
1550
+ /** Human-facing copy for what the customer must do next. */
1551
+ instruction?: string;
1552
+ }
1553
+ /**
1554
+ * Result of an ACCEPTED domain offboard (`DELETE /v1/domains/{domain}` → HTTP 202).
1555
+ * Teardown — reaping the outbound provider senders + routing rows, then scrubbing
1556
+ * the DNS zone/records and the domain row — runs as an async job. Poll `status_url`
1557
+ * (`GET /v1/jobs/{job_id}`, via {@link Job} / `client.getJob(job_id)`) until
1558
+ * `status` is terminal (succeeded/failed/cancelled); the domain is ACCEPTED for
1559
+ * offboard, not yet fully torn down when this returns.
1560
+ */
1561
+ interface DomainOffboard {
1562
+ domain: string;
1563
+ job_id: string;
1564
+ status: string;
1565
+ status_url: string;
1566
+ }
1567
+ /**
1568
+ * Poll-loop status for one async job (currently only the domain-offboard
1569
+ * teardown started by `domains.offboard`). Mirrors `GET /v1/jobs/{job_id}`.
1570
+ * `status` is terminal on succeeded/failed/cancelled; keep polling otherwise.
1571
+ */
1572
+ interface Job {
1573
+ object: "job";
1574
+ id: string;
1575
+ type: string;
1576
+ status: string;
1577
+ created_at: IsoTimestamp;
1578
+ updated_at: IsoTimestamp;
1579
+ finished_at?: IsoTimestamp;
1580
+ }
1581
+ /**
1582
+ * One event from the SSE stream (`GET /v1/inboxes/{addr}/stream` or `GET
1583
+ * /v1/events`). It is the SAME envelope a webhook delivers, so a stream consumer
1584
+ * and a webhook consumer see identical data. `seq` is the monotonic resume token
1585
+ * (the SSE frame's `id:` field): pass the last `seq` you saw as `lastEventId` on
1586
+ * reconnect to replay only events after it. The envelope is intentionally generic
1587
+ * so it can also carry future event types (e.g. HITL).
1588
+ */
1589
+ interface StreamEvent {
1590
+ /** Event type, e.g. `message.received`. Mirrors the SSE `event:` field. */
1591
+ event: string;
1592
+ /** Monotonic resume token (the SSE `id:` field). Pass back as `lastEventId`. */
1593
+ seq: number;
1594
+ /** Opaque event id (`evt_...`). */
1595
+ id: string;
1596
+ created_at: IsoTimestamp;
1597
+ /** The inbox this event concerns. */
1598
+ inbox: string;
1599
+ /** The message the event is about (present for message.* events). */
1600
+ message?: Message;
1601
+ }
1602
+ /** Options for the SSE stream / subscribe helpers. */
1603
+ interface StreamOptions {
1604
+ /**
1605
+ * Resume token from a prior run: the `seq` of the last event you processed. The
1606
+ * server replays every event after it (Last-Event-ID semantics), so a reconnect
1607
+ * never misses or double-delivers. Omit to start from now.
1608
+ */
1609
+ lastEventId?: number;
1610
+ /** Abort the stream (close the connection) when this signal fires. */
1611
+ signal?: AbortSignal;
1612
+ }
1613
+ /** Request body for the unauthenticated `POST /v1/agent/sign-up`. */
1614
+ interface SignUpRequest {
1615
+ /** Human email that receives the one-time verification code. */
1616
+ human_email: string;
1617
+ /** Desired local-part for the first inbox (optional; auto-generated when omitted). */
1618
+ username?: string;
1619
+ }
1620
+ /**
1621
+ * Response from `POST /v1/agent/sign-up`. The `agent_key` is a LIMITED-scope key
1622
+ * (read-only) until the emailed code is confirmed via `POST /v1/agent/verify`.
1623
+ * The OTP itself is never returned — it is emailed to `human_email`.
1624
+ */
1625
+ interface SignUpResponse {
1626
+ customer_id: string;
1627
+ agent_id: string;
1628
+ /** Limited-scope agent key, shown once. Re-calling signup rotates it. */
1629
+ agent_key: string;
1630
+ key_prefix: string;
1631
+ scopes: Scope[];
1632
+ /** The first inbox minted for the agent. */
1633
+ address: string;
1634
+ verified: boolean;
1635
+ /** Where the verification code was sent. */
1636
+ otp_sent_to: string;
1637
+ otp_expires_at: IsoTimestamp;
1638
+ message: string;
1639
+ }
1640
+ /** Request body for `POST /v1/agent/verify`. */
1641
+ interface VerifyRequest {
1642
+ /** The one-time code delivered to the signup human email. */
1643
+ otp: string;
1644
+ }
1645
+ /**
1646
+ * Response from `POST /v1/agent/verify`. On success a NEW full-scope `agent_key`
1647
+ * is returned (shown once); switch to it for subsequent calls.
1648
+ */
1649
+ interface VerifyResponse {
1650
+ agent_id: string;
1651
+ agent_key: string;
1652
+ key_prefix: string;
1653
+ scopes: Scope[];
1654
+ verified: boolean;
1655
+ message: string;
1656
+ }
1657
+ /**
1658
+ * Response from `GET /v1/auth/me` — the verified principal behind the key.
1659
+ *
1660
+ * `org_id`/`project_id` are the FIXED org/project the key is bound to (resolved from
1661
+ * the stored key, never client input). There is NO mutable project selector for a
1662
+ * scoped key — `whoami` is the canonical project-visibility surface; project
1663
+ * selection happens when the human/admin issues the enrollment token or agent key.
1664
+ */
1665
+ interface WhoAmI {
1666
+ customer_id: string;
1667
+ /**
1668
+ * The fixed org the key is bound to. Optional to match the OpenAPI contract: a
1669
+ * legacy/unscoped server (or any deploy predating org/project binding) may omit it.
1670
+ */
1671
+ org_id?: string;
1672
+ /**
1673
+ * The fixed project the key is bound to. Optional to match the OpenAPI contract: a
1674
+ * legacy/unscoped server (or any deploy predating org/project binding) may omit it.
1675
+ */
1676
+ project_id?: string;
1677
+ agent_id: string;
1678
+ key_id: string;
1679
+ scopes: Scope[];
1680
+ }
1681
+
1682
+ /**
1683
+ * The ONE list envelope + opaque-cursor iteration (redesign §5.2 / §6.2).
1684
+ *
1685
+ * Every redesign collection endpoint (the canonical `x.projects.inboxes.*` chain
1686
+ * and beyond) returns {@link List}: `{ object: "list", data, has_more, next_cursor }`.
1687
+ * `next_cursor` is OPAQUE — treat it as a token and pass it back verbatim as
1688
+ * `?cursor` to fetch the next page. {@link ListPage} wraps a raw {@link List} with
1689
+ * ergonomic iteration (`for await … of`) and a `nextPage()` cursor walker so callers
1690
+ * never thread cursors by hand.
1691
+ */
1692
+ /** The opaque pagination cursor. Pass it back verbatim as `?cursor`; never parse it. */
1693
+ type Cursor = string;
1694
+ /** The one collection envelope for every redesign list response. */
1695
+ interface List<T> {
1696
+ /** Always the literal `"list"`. */
1697
+ object: "list";
1698
+ /** The rows on this page. */
1699
+ data: T[];
1700
+ /** True when another page exists (i.e. `next_cursor` is non-null). */
1701
+ has_more: boolean;
1702
+ /** Opaque cursor for the next page, or `null` on the last page. */
1703
+ next_cursor: Cursor | null;
1704
+ }
1705
+ /** Query params shared by every cursor-paginated list. */
1706
+ interface ListParams {
1707
+ /** Page size (server clamps to 1–100; default 50). */
1708
+ limit?: number;
1709
+ /** Opaque cursor from a prior page's `next_cursor`. */
1710
+ cursor?: Cursor;
1711
+ }
1712
+ /** A function that fetches one page given an opaque cursor (or undefined for page 1). */
1713
+ type PageFetcher<T> = (cursor: Cursor | undefined, signal?: AbortSignal) => Promise<List<T>>;
1714
+ /**
1715
+ * An ergonomic wrapper over a single {@link List} page that also knows how to fetch
1716
+ * the next page and auto-iterate across ALL pages.
1717
+ *
1718
+ * ```ts
1719
+ * const page = await x.projects.inboxes.list("proj_9k");
1720
+ * for await (const inbox of page) console.log(inbox.id); // walks every page
1721
+ * // …or page-at-a-time:
1722
+ * if (page.hasMore) { const next = await page.nextPage(); }
1723
+ * ```
1724
+ */
1725
+ declare class ListPage<T> implements AsyncIterable<T> {
1726
+ private readonly fetcher;
1727
+ /** The rows on THIS page. */
1728
+ readonly data: T[];
1729
+ /** True when another page exists. */
1730
+ readonly hasMore: boolean;
1731
+ /** Opaque cursor for the next page (null on the last page). */
1732
+ readonly nextCursor: Cursor | null;
1733
+ /** Always `"list"`. */
1734
+ readonly object: "list";
1735
+ constructor(raw: List<T>, fetcher: PageFetcher<T>);
1736
+ /**
1737
+ * Fetch the next page. Throws if there is none — guard with {@link hasMore}.
1738
+ */
1739
+ nextPage(signal?: AbortSignal): Promise<ListPage<T>>;
1740
+ /**
1741
+ * Auto-paginate: yield every row across every page, fetching subsequent pages
1742
+ * lazily as the iterator is consumed. The default iteration of a `ListPage`.
1743
+ */
1744
+ [Symbol.asyncIterator](): AsyncIterator<T>;
1745
+ /** Collect EVERY row across EVERY page into a single array (eager). */
1746
+ collect(signal?: AbortSignal): Promise<T[]>;
1747
+ }
1748
+ /** Build a {@link ListPage} from the first raw {@link List} and a page fetcher. */
1749
+ declare function listPage<T>(raw: List<T>, fetcher: PageFetcher<T>): ListPage<T>;
1750
+
1751
+ /**
1752
+ * Transport abstraction.
1753
+ *
1754
+ * The resource methods speak a small, typed RPC surface; a transport decides whether each call hits
1755
+ * the live Extrovert API over HTTP or the offline {@link MockBackend}. This is the single mock seam:
1756
+ * flip `transport: "mock"` (or `EXTROVERT_API_BASE_URL=mock`) and the whole SDK runs without a network.
1757
+ *
1758
+ * `HttpTransport` is used for the live API; the mock stays for examples, docs, and tests.
1759
+ */
1760
+
1761
+ /** Raw bytes of one attachment plus the metadata needed to save/serve it. */
1762
+ interface AttachmentDownload {
1763
+ filename: string;
1764
+ content_type: string;
1765
+ /** Standard base64 of the attachment bytes. */
1766
+ content_base64: string;
1767
+ }
1768
+ /** The RPC surface every transport implements. One method per spec §8 endpoint. */
1769
+ interface Transport {
1770
+ enroll(req: EnrollRequest, signal?: AbortSignal): Promise<EnrollResponse>;
1771
+ signUp(req: SignUpRequest, signal?: AbortSignal): Promise<SignUpResponse>;
1772
+ verify(req: VerifyRequest, signal?: AbortSignal): Promise<VerifyResponse>;
1773
+ whoami(signal?: AbortSignal): Promise<WhoAmI>;
1774
+ createInbox(req: CreateInboxRequest, idempotencyKey?: string, signal?: AbortSignal): Promise<Inbox>;
1775
+ listInboxes(params: ListInboxesParams, signal?: AbortSignal): Promise<Page<Inbox>>;
1776
+ getInbox(address: string, signal?: AbortSignal): Promise<Inbox>;
1777
+ updateInbox(address: string, req: UpdateInboxRequest, signal?: AbortSignal): Promise<Inbox>;
1778
+ deleteInbox(address: string, signal?: AbortSignal): Promise<void>;
1779
+ createInboxInProject(projectId: string, req: CreateInboxRequest, idempotencyKey?: string, signal?: AbortSignal): Promise<Inbox>;
1780
+ listInboxesInProject(projectId: string, params: ProjectInboxListParams, signal?: AbortSignal): Promise<List<Inbox>>;
1781
+ getInboxInProject(projectId: string, inboxId: string, params: GetInboxParams, signal?: AbortSignal): Promise<Inbox>;
1782
+ updateInboxInProject(projectId: string, inboxId: string, req: UpdateInboxRequest, signal?: AbortSignal): Promise<Inbox>;
1783
+ deleteInboxInProject(projectId: string, inboxId: string, signal?: AbortSignal): Promise<void>;
1784
+ getInboxCredentialsInProject(projectId: string, inboxId: string, signal?: AbortSignal): Promise<InboxCredentials>;
1785
+ send(address: string, req: SendRequest, signal?: AbortSignal): Promise<SendOutcome>;
1786
+ reply(address: string, req: ReplyRequest, signal?: AbortSignal): Promise<SendOutcome>;
1787
+ forward(address: string, messageId: string, req: ForwardRequest, signal?: AbortSignal): Promise<SendOutcome>;
1788
+ listMessages(address: string, params: ListMessagesParams, signal?: AbortSignal): Promise<Page<Message>>;
1789
+ getMessage(messageId: string, signal?: AbortSignal): Promise<Message>;
1790
+ getMessageRaw(address: string, messageId: string, signal?: AbortSignal): Promise<string>;
1791
+ listAttachments(address: string, messageId: string, signal?: AbortSignal): Promise<Page<Attachment>>;
1792
+ getAttachment(address: string, messageId: string, attachmentId: string, signal?: AbortSignal): Promise<AttachmentDownload>;
1793
+ markRead(address: string, messageId: string, req: MarkReadRequest, signal?: AbortSignal): Promise<Message>;
1794
+ deleteMessage(address: string, messageId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
1795
+ batchUpdateMessages(address: string, req: BatchUpdateMessagesRequest, signal?: AbortSignal): Promise<BatchUpdateResult>;
1796
+ searchMessages(address: string, params: SearchMessagesParams, signal?: AbortSignal): Promise<Page<Message>>;
1797
+ listThreads(address: string, params: ListThreadsParams, signal?: AbortSignal): Promise<Page<Thread>>;
1798
+ searchThreads(address: string, params: SearchMessagesParams, signal?: AbortSignal): Promise<Page<Thread>>;
1799
+ getThread(address: string, threadId: string, signal?: AbortSignal): Promise<ThreadDetail>;
1800
+ deleteThread(address: string, threadId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
1801
+ waitForEmail(address: string, req: WaitForEmailRequest, timeoutMs: number, signal?: AbortSignal): Promise<WaitForEmailResult>;
1802
+ registerWebhook(req: RegisterWebhookRequest, signal?: AbortSignal): Promise<Webhook>;
1803
+ listWebhooks(signal?: AbortSignal): Promise<Page<Webhook>>;
1804
+ getWebhook(webhookId: string, signal?: AbortSignal): Promise<Webhook>;
1805
+ updateWebhook(webhookId: string, req: UpdateWebhookRequest, signal?: AbortSignal): Promise<Webhook>;
1806
+ deleteWebhook(webhookId: string, signal?: AbortSignal): Promise<void>;
1807
+ addContactListEntry(address: string, req: AddContactListRequest, signal?: AbortSignal): Promise<ContactListEntry>;
1808
+ listContactLists(address: string, signal?: AbortSignal): Promise<Page<ContactListEntry>>;
1809
+ deleteContactListEntry(address: string, entryId: string, signal?: AbortSignal): Promise<void>;
1810
+ precheckSuppression(recipient: string, signal?: AbortSignal): Promise<SuppressionPrecheck>;
1811
+ listSuppressions(params: ListSuppressionsParams, signal?: AbortSignal): Promise<Page<SuppressionEntry>>;
1812
+ revokeSuppression(id: string, reason: string, signal?: AbortSignal): Promise<SuppressionEntry>;
1813
+ listDomains(signal?: AbortSignal): Promise<Page<Domain>>;
1814
+ getDomain(domain: string, signal?: AbortSignal): Promise<Domain>;
1815
+ onboardDomain(req: OnboardDomainRequest, signal?: AbortSignal): Promise<Domain>;
1816
+ verifyDomain(domain: string, signal?: AbortSignal): Promise<Domain>;
1817
+ offboardDomain(domain: string, signal?: AbortSignal): Promise<DomainOffboard>;
1818
+ getJob(jobId: string, signal?: AbortSignal): Promise<Job>;
1819
+ submitForReview(address: string, req: SendRequest, signal?: AbortSignal): Promise<SendOutcome>;
1820
+ submitReplyForReview(address: string, req: ReplyRequest, signal?: AbortSignal): Promise<SendOutcome>;
1821
+ listReviews(params: ListReviewsParams, signal?: AbortSignal): Promise<Page<Review>>;
1822
+ getReview(reviewId: string, signal?: AbortSignal): Promise<Review>;
1823
+ getReviewTurns(reviewId: string, signal?: AbortSignal): Promise<Page<ReviewTurn>>;
1824
+ getReviewFeedback(reviewId: string, signal?: AbortSignal): Promise<ReviewFeedback>;
1825
+ postReviewChat(reviewId: string, req: PostReviewChatRequest, idempotencyKey?: string, signal?: AbortSignal): Promise<Review>;
1826
+ submitRevision(reviewId: string, req: SubmitRevisionRequest, signal?: AbortSignal): Promise<Review>;
1827
+ cancelReview(reviewId: string, idempotencyKey?: string, signal?: AbortSignal): Promise<Review>;
1828
+ restampReview(reviewId: string, req: RestampReviewRequest, signal?: AbortSignal): Promise<Review>;
1829
+ getReviewDecisionContext(reviewId: string, signal?: AbortSignal): Promise<ReviewDecisionContext>;
1830
+ reviewerDecide(reviewId: string, req: ReviewerDecisionRequest, signal?: AbortSignal): Promise<ReviewerDecisionResult>;
1831
+ listReviewEvents(params: ListReviewEventsParams, signal?: AbortSignal): Promise<ReviewEventsResult>;
1832
+ waitForReviewEvent(params: WaitForReviewEventParams, signal?: AbortSignal): Promise<ReviewEventsResult>;
1833
+ ackReviewEvent(req: AckReviewEventRequest, signal?: AbortSignal): Promise<AckReviewEventResult>;
1834
+ listCategories(params: ListCategoriesParams, signal?: AbortSignal): Promise<Page<Category>>;
1835
+ getCategory(categoryId: string, signal?: AbortSignal): Promise<Category>;
1836
+ proposeCategory(req: ProposeCategoryRequest, signal?: AbortSignal): Promise<Category>;
1837
+ updateCategory(categoryId: string, req: UpdateCategoryRequest, signal?: AbortSignal): Promise<Category>;
1838
+ getRiskDial(signal?: AbortSignal): Promise<RiskDial>;
1839
+ getGraduationStatus(categoryId: string, signal?: AbortSignal): Promise<GraduationStatus>;
1840
+ proposeGraduation(categoryId: string, req: ProposeGraduationRequest, signal?: AbortSignal): Promise<GraduationStatus>;
1841
+ getScanBacklogStatus(categoryId: string, signal?: AbortSignal): Promise<ScanBacklogStatus>;
1842
+ getCategoryPacingState(categoryId: string, signal?: AbortSignal): Promise<CategoryPacingState>;
1843
+ getRules(params: GetRulesParams, signal?: AbortSignal): Promise<Page<Rule>>;
1844
+ saveRule(req: SaveRuleRequest, signal?: AbortSignal): Promise<Rule>;
1845
+ promoteRule(ruleId: string, toScope: "general" | "category", signal?: AbortSignal): Promise<Rule>;
1846
+ retireRule(ruleId: string, signal?: AbortSignal): Promise<Rule>;
1847
+ getRuleAudit(params: GetRuleAuditParams, signal?: AbortSignal): Promise<Page<RuleAuditEntry>>;
1848
+ undoRuleChange(udoId: string, signal?: AbortSignal): Promise<Rule>;
1849
+ /**
1850
+ * Open a live event stream. `address` scopes to one inbox (`GET
1851
+ * /v1/inboxes/{addr}/stream`); pass `null` for every owned inbox (`GET
1852
+ * /v1/events`). `lastEventId`, when set, resumes after that event seq. The
1853
+ * generator ends when the server closes the stream or `signal` aborts.
1854
+ */
1855
+ stream(address: string | null, lastEventId?: number, signal?: AbortSignal): AsyncGenerator<StreamEvent, void, unknown>;
1856
+ }
1857
+
1858
+ /**
1859
+ * Offline fixtures for the Extrovert SDK.
1860
+ *
1861
+ * The SDK can run against this in-memory, deterministic mock so examples, the MCP server, and the
1862
+ * console are fully navigable offline without contacting the live `/v1` API. Construct a client with
1863
+ * `transport: "mock"` (or set `EXTROVERT_API_BASE_URL=mock`) to route requests here instead of fetch.
1864
+ *
1865
+ * The mock honors the same request/response models as the real API and reproduces the few behaviors
1866
+ * the SDK ergonomics depend on (enrollment cap, idempotency on `client_id`, wait_for_email returning
1867
+ * an OTP). It is intentionally simple — not a full server — and never reaches the network.
1868
+ */
1869
+
1870
+ /**
1871
+ * A self-contained, deterministic-enough in-memory backend implementing the subset of behavior the
1872
+ * SDK exposes. One instance per mock client so tests/examples don't bleed into each other.
1873
+ */
1874
+ declare class MockBackend {
1875
+ private state;
1876
+ reset(): void;
1877
+ enroll(req: EnrollRequest): EnrollResponse;
1878
+ /**
1879
+ * Self-signup (Slice E). Returns a LIMITED-scope key and a first inbox; the OTP
1880
+ * is held in-memory (mock only) so `verify` can elevate to full scope. Idempotent
1881
+ * on human_email: a re-call reuses the tenant/agent and rotates the OTP.
1882
+ */
1883
+ signUp(req: SignUpRequest): SignUpResponse;
1884
+ /** Confirm a signup OTP and return a full-scope key (mock). */
1885
+ verify(req: VerifyRequest): VerifyResponse;
1886
+ /** Introspect the mock principal (GET /v1/auth/me). */
1887
+ whoami(): WhoAmI;
1888
+ createInbox(req: CreateInboxRequest): Inbox;
1889
+ listInboxes(params?: ListInboxesParams): Page<Inbox>;
1890
+ getInbox(address: string): Inbox | undefined;
1891
+ /**
1892
+ * Normalize an inbox ref (opaque id OR address alias) to the canonical address the
1893
+ * mock keys its message/thread/contact maps on. The SDK now routes inbox ops by the
1894
+ * canonical opaque `id` when it holds a full record (matching the contract's
1895
+ * canonical-key semantics), so the mock must resolve an id back to its address —
1896
+ * both key `state.inboxes` (same object), `state.messages` keys by address only.
1897
+ * Unknown refs pass through unchanged so the existing not-found paths still fire.
1898
+ */
1899
+ private addrOf;
1900
+ updateInbox(address: string, req: UpdateInboxRequest): Inbox | undefined;
1901
+ deleteInbox(address: string): void;
1902
+ /** Resolve the project path segment against the mock's bound project (fail-closed). */
1903
+ private resolveProjectSegment;
1904
+ createInboxInProject(projectId: string, req: CreateInboxRequest): Inbox;
1905
+ listInboxesInProject(projectId: string, params?: ProjectInboxListParams): List<Inbox>;
1906
+ getInboxInProject(projectId: string, inboxId: string): Inbox | undefined;
1907
+ updateInboxInProject(projectId: string, inboxId: string, req: UpdateInboxRequest): Inbox | undefined;
1908
+ deleteInboxInProject(projectId: string, inboxId: string): boolean;
1909
+ getInboxCredentialsInProject(projectId: string, inboxId: string): InboxCredentials | undefined;
1910
+ /** The RESOLVED review policy for an inbox: per-inbox override, else the account default. */
1911
+ reviewPolicyFor(address: string): ReviewPolicy;
1912
+ /**
1913
+ * Mock-only: set the ACCOUNT-level review policy (the console/admin plane sets
1914
+ * this on the real server; there is no agent-plane write for it). Use it to
1915
+ * exercise an org that has been configured for direct sending.
1916
+ */
1917
+ setReviewPolicy(policy: ReviewPolicy): void;
1918
+ /** Mock-only: set (or clear) the per-inbox override, which beats the account default. */
1919
+ setInboxReviewPolicy(address: string, policy: ReviewPolicy | undefined): void;
1920
+ send(address: string, req: SendRequest): SendOutcome;
1921
+ reply(address: string, req: ReplyRequest): SendOutcome;
1922
+ forward(address: string, messageId: string, req: ForwardRequest): SendOutcome;
1923
+ /**
1924
+ * Submit a new message for review (mock). Rides the SAME endpoint as `send` on
1925
+ * the real server, so it is literally the same call here: the resolved policy —
1926
+ * not which SDK method you picked — decides whether the message is queued
1927
+ * (`kind:"queued_for_review"`) or delivered.
1928
+ */
1929
+ submitForReview(address: string, req: SendRequest): SendOutcome;
1930
+ /** Submit an in-thread reply for review (mock). Same endpoint, same routing, as `reply`. */
1931
+ submitReplyForReview(address: string, req: ReplyRequest): SendOutcome;
1932
+ /**
1933
+ * The single enforcement point. Everything above funnels here; nothing else in
1934
+ * the mock may reach `deliver*` on the agent plane.
1935
+ */
1936
+ private submitOutbound;
1937
+ /**
1938
+ * Move a review to `state` and keep `closed` in lockstep.
1939
+ *
1940
+ * `closed` is DERIVED, never independently assigned, because the two drifting
1941
+ * apart is worse than either being wrong: an agent that polls `closed` on a row
1942
+ * whose state says otherwise has no way to tell which one to believe.
1943
+ */
1944
+ private setReviewState;
1945
+ /** Mark a review delivered on an auto-send path and emit its terminal `sent` nudge. */
1946
+ private markReviewAutoSent;
1947
+ /** Raw delivery for a send — no policy, only reachable from submitOutbound. */
1948
+ private deliverSend;
1949
+ /** Raw delivery for a reply — no policy, only reachable from submitOutbound. */
1950
+ private deliverReply;
1951
+ /** Append the outbound message and shape the legacy send result. */
1952
+ private deliverRaw;
1953
+ /**
1954
+ * Derive a reply's recipients / `Re:` subject / thread from the parent, the way
1955
+ * the server does before it writes the review row.
1956
+ */
1957
+ private deriveReplyEnvelope;
1958
+ /** List review requests (mock), newest-first, with optional filters. */
1959
+ listReviews(params?: ListReviewsParams): Page<Review>;
1960
+ /** Get one review request (mock), or undefined when not found. */
1961
+ getReview(reviewId: string): Review | undefined;
1962
+ /** Get a review's append-only thread turns (mock), or undefined when not found. */
1963
+ getReviewTurns(reviewId: string): Page<ReviewTurn> | undefined;
1964
+ /**
1965
+ * Get the human's assembled feedback (mock; M5): the diff + the human comments /
1966
+ * rejection turns + the decision (derived from state). new_rules is empty in the
1967
+ * mock (rule provenance lives server-side). Undefined when not found.
1968
+ */
1969
+ getReviewFeedback(reviewId: string): ReviewFeedback | undefined;
1970
+ /**
1971
+ * Post a chat turn on a review's thread (mock; M5). Idempotent on the optional key
1972
+ * (parity v21): a replay with the same key returns the same review without doubling
1973
+ * the turn. Flips in_review -> chatting on the first turn. Undefined when not found.
1974
+ */
1975
+ postReviewChat(reviewId: string, req: PostReviewChatRequest, idempotencyKey?: string): Review | undefined;
1976
+ /**
1977
+ * Post a new agent draft under a parent_revision CAS (mock; M5; D17). A mismatch is
1978
+ * a 409 STALE with NO mutation; a clean CAS re-renders the draft in place
1979
+ * (revision++), returns to needs_review. Undefined when not found.
1980
+ */
1981
+ submitRevision(reviewId: string, req: SubmitRevisionRequest): Review | undefined;
1982
+ /** Withdraw a pending review (mock; M5) to the terminal cancelled state. */
1983
+ cancelReview(reviewId: string): Review | undefined;
1984
+ /**
1985
+ * Re-stamp a draft's rules-version WITHOUT redrafting (mock; D19/§8 $0 escape valve).
1986
+ * Advances the version the draft is current against; no revision bump, no body change.
1987
+ * A terminal/sent draft 409s; against_version above the category's current
1988
+ * rules-version is 400. The mock has no per-category rules_version on the Review, so it
1989
+ * just bumps the row version and returns it (the body is untouched).
1990
+ */
1991
+ restampReview(reviewId: string, req: RestampReviewRequest): Review | undefined;
1992
+ /**
1993
+ * Test helper: create a draft already ASSIGNED to a reviewer (in_review), mirroring
1994
+ * how the real server routes a parked draft to a linked review-agent. Returns the
1995
+ * review id so a test can drive the reviewer decision plane offline. `createdAtMs`
1996
+ * (optional) backdates created_at so a test can trip the hard review_deadline breaker.
1997
+ */
1998
+ seedReviewerHeldReview(opts?: {
1999
+ fromAddress: string;
2000
+ createdAtMs?: number;
2001
+ }): string;
2002
+ /** Get the reviewer's decision context for a review (mock; §9). */
2003
+ getReviewDecisionContext(reviewId: string): ReviewDecisionContext | undefined;
2004
+ /**
2005
+ * Submit a reviewer decision (mock; §9). approve/edit → the platform "sends" with the
2006
+ * composer's creds (kind=sent, send_path=reviewer_approved); reject → back to the
2007
+ * composer (needs_review, hop_count++) UNLESS a breaker forces the human; escalate →
2008
+ * the human queue. revision/version are the CAS (409 STALE on mismatch, NO mutation).
2009
+ */
2010
+ reviewerDecide(reviewId: string, req: ReviewerDecisionRequest): ReviewerDecisionResult | undefined;
2011
+ /**
2012
+ * Browse the registry (mock), newest-first, excluding merged/soft-deleted. `match`
2013
+ * is a pure lexical filter (every token must appear in name+description) — NO LLM,
2014
+ * mirroring the server.
2015
+ */
2016
+ listCategories(params?: ListCategoriesParams): Page<Category>;
2017
+ /** Get one category (mock), or undefined when not found. */
2018
+ getCategory(categoryId: string): Category | undefined;
2019
+ /** Propose a category (mock): stands immediately, author_kind=agent (D9). */
2020
+ proposeCategory(req: ProposeCategoryRequest): Category;
2021
+ /** Rename / re-describe a category (mock) — metadata only (D10). */
2022
+ updateCategory(categoryId: string, req: UpdateCategoryRequest): Category | undefined;
2023
+ /** The mock account-default risk dial (mirrors the server defaults). */
2024
+ private accountDial;
2025
+ /**
2026
+ * Read the effective risk dial (mock): the account default + every category with an
2027
+ * inherited (null override) effective dial. The mock category carries no overrides,
2028
+ * so every category inherits — effective == account.
2029
+ */
2030
+ getRiskDial(): RiskDial;
2031
+ private nextGraduationState;
2032
+ /**
2033
+ * Read a category's graduation gate status (mock). The mock category has no
2034
+ * counters, so it reports zero clean approvals / zero drift against the account
2035
+ * defaults; can_graduate is true for supervised→auto_notify (no maturity gate).
2036
+ */
2037
+ getGraduationStatus(categoryId: string): GraduationStatus | undefined;
2038
+ /**
2039
+ * Propose graduating a category (mock): returns the current gate status without
2040
+ * changing the category state (D16 — an agent can never flip the bit).
2041
+ */
2042
+ proposeGraduation(categoryId: string, _req: ProposeGraduationRequest): GraduationStatus | undefined;
2043
+ /**
2044
+ * Read the D19/§8 backlog-reconciliation status (mock): counts the QUEUED drafts in a
2045
+ * category that are stale vs current-enough against the current rules-version. The
2046
+ * mock has no per-draft composed_* stamps on its Review fixtures, so every queued
2047
+ * draft reads as current-enough (composed 0 vs current 0) — the contract shape is
2048
+ * exercised; the integer-compare logic is covered by the Go tests.
2049
+ */
2050
+ getScanBacklogStatus(categoryId: string): ScanBacklogStatus | undefined;
2051
+ /**
2052
+ * Read the demand-driven pacing state (mock — M7 Slice B/§8): the cursor + effective
2053
+ * window/ceiling/interval + each queued draft's classification. The mock has no cursor
2054
+ * (nothing reviewed) and no composed_* stamps, so every queued draft reads in-window-
2055
+ * fresh until the window fills, then ahead; the contract shape is exercised (the
2056
+ * cursor/staleness mechanics are covered by the Go tests).
2057
+ */
2058
+ getCategoryPacingState(categoryId: string): CategoryPacingState | undefined;
2059
+ /** rank a rule for the §7 ladder (mock mirror of the server's deterministic order). */
2060
+ private ruleRank;
2061
+ /** Get the ORDERED active rule set (mock) — §7 ladder + category-before-general. */
2062
+ getRules(params?: GetRulesParams): Page<Rule>;
2063
+ /** Save / edit a rule (mock) — append-only by supersession (D11). */
2064
+ saveRule(req: SaveRuleRequest): Rule;
2065
+ /** Promote a rule between layers (mock, via supersession), or undefined when unknown. */
2066
+ promoteRule(ruleId: string, toScope: "general" | "category"): Rule | undefined;
2067
+ /** Retire a rule (mock) — soft delete, or undefined when unknown. */
2068
+ retireRule(ruleId: string): Rule | undefined;
2069
+ /** Read the rule/category change audit log (mock). */
2070
+ getRuleAudit(params?: GetRuleAuditParams): Page<RuleAuditEntry>;
2071
+ /** Undo a rule change (mock) — restore the prior version; idempotent (re-undo 409). */
2072
+ undoRuleChange(udoId: string): Rule;
2073
+ /** recordRuleAudit appends one change/undo audit row (mock). */
2074
+ private recordRuleAudit;
2075
+ /**
2076
+ * enqueueReviewEvent appends a durable nudge for a review with the next per-review
2077
+ * monotonic seq (mock mirror of the server's enqueue-on-transition).
2078
+ */
2079
+ private enqueueReviewEvent;
2080
+ /**
2081
+ * Emit the terminal-class nudge for the review's current state. `sent` and
2082
+ * `cancelled` close the row. `failed` emits `send_failed`, and a later explicit
2083
+ * cancel emits `cancelled`; this helper deliberately does not claim the first is
2084
+ * the last event the review can produce.
2085
+ */
2086
+ private enqueueTerminalNudge;
2087
+ /**
2088
+ * Enqueue `front_run_next` — the signal that the review reached a terminal state
2089
+ * while the agent was still trying to act on it.
2090
+ *
2091
+ * Deduped on (review, terminal state, parent revision) so a retry loop hitting
2092
+ * the same 409 collapses to ONE row rather than one per attempt. Best-effort by
2093
+ * design: it must never turn a 409 into a failure.
2094
+ */
2095
+ private enqueueFrontRunNudge;
2096
+ /**
2097
+ * Mock-only: mirror a console/human approve that delivers the draft. The live
2098
+ * server does this on the console plane (never an SDK call); the hook exists so a
2099
+ * drain loop can be driven to its terminal `sent` event offline.
2100
+ */
2101
+ simulateReviewApproved(reviewId: string, opts?: {
2102
+ edited?: boolean;
2103
+ }): Review | undefined;
2104
+ /**
2105
+ * Mock-only: mirror an approved draft whose delivery then FAILED at the provider.
2106
+ * This is the case the composing agent was previously never told about — the
2107
+ * console showed the error and the agent's queue stayed silent — so the loop test
2108
+ * that matters most drives this path.
2109
+ */
2110
+ simulateSendFailed(reviewId: string, error?: string): Review | undefined;
2111
+ /**
2112
+ * simulateReviewRejected is a mock-only test hook that mirrors a console reject:
2113
+ * it enqueues a `rejected` durable nudge to the composer so the realtime
2114
+ * drain/ack surface has a deterministic event to exercise offline. The live
2115
+ * server enqueues this on the reject TRANSITION (console-only, never an SDK call).
2116
+ */
2117
+ simulateReviewRejected(reviewId: string, comment?: string): ReviewEvent;
2118
+ /**
2119
+ * simulateReviewOpened is a mock-only test hook that mirrors a reviewer opening a
2120
+ * draft (needs_review -> in_review). There is no agent-plane "open for review"
2121
+ * call (a reviewer/human opens it), so tests of the M5 chat surface use this to get
2122
+ * a chattable draft offline.
2123
+ */
2124
+ simulateReviewOpened(reviewId: string): Review | undefined;
2125
+ /**
2126
+ * simulateHumanComment is a mock-only test hook that mirrors the console chat side-
2127
+ * panel: it appends a human_comment turn so get_review_feedback has a comment to
2128
+ * assemble. The live server writes this on the console plane (never an SDK call).
2129
+ */
2130
+ simulateHumanComment(reviewId: string, body: string): ReviewTurn | undefined;
2131
+ /** Drain the next un-acked review events (mock), FIFO per review + cursors. */
2132
+ listReviewEvents(params?: ListReviewEventsParams): ReviewEventsResult;
2133
+ /**
2134
+ * Long-poll for a review event (mock). Offline there is nothing to wait FOR, so it
2135
+ * returns the immediate drain (empty when caught up) — the server's "empty on
2136
+ * timeout" contract.
2137
+ */
2138
+ waitForReviewEvent(params?: WaitForReviewEventParams): ReviewEventsResult;
2139
+ /** Ack review events (mock): advance per-review cursors monotonically. */
2140
+ ackReviewEvent(req: AckReviewEventRequest): AckReviewEventResult;
2141
+ /** Mint a needs_review record + its intent (agent_note) and initial-draft turns. */
2142
+ private createReviewRecord;
2143
+ /** Append a message to an inbox's store and return it (mock helper). */
2144
+ private appendMessage;
2145
+ /** List a message's attachment metadata (mirrors the list endpoint). */
2146
+ listAttachments(messageId: string): Page<Attachment>;
2147
+ /** Fetch one attachment's bytes + metadata (mirrors the download endpoint). */
2148
+ getAttachment(messageId: string, attachmentId: string): AttachmentDownload;
2149
+ listMessages(address: string, params?: ListMessagesParams): Page<Message>;
2150
+ /** Fetch a single message by id across all inboxes. */
2151
+ getMessage(messageId: string): Message;
2152
+ /** Toggle the \Seen flag for a message by id. */
2153
+ markRead(messageId: string, read: boolean): Message;
2154
+ /** Render a minimal RFC822 .eml for a message by id (mirrors the raw endpoint). */
2155
+ getMessageRaw(messageId: string): string;
2156
+ /** Full-text search scoped to one inbox. */
2157
+ searchMessages(address: string, params: SearchMessagesParams): Page<Message>;
2158
+ listThreads(address: string): Page<Thread>;
2159
+ /** Thread-level search (subject / snippet / participant substring). */
2160
+ searchThreads(address: string, params: SearchMessagesParams): Page<Thread>;
2161
+ /** Fetch one thread (with messages, oldest-first) by id under an inbox. */
2162
+ getThread(address: string, threadId: string): ThreadDetail;
2163
+ /**
2164
+ * Delete a message by id: move it to Trash (soft) or remove it (expunge / it
2165
+ * already lives in Trash). Returns undefined when the inbox/message is unknown
2166
+ * so the transport can surface a 404.
2167
+ */
2168
+ deleteMessage(address: string, messageId: string, expunge: boolean): DeleteResult | undefined;
2169
+ /** Delete every message in a thread by id (move to Trash or expunge). */
2170
+ deleteThread(address: string, threadId: string, expunge: boolean): DeleteResult | undefined;
2171
+ /**
2172
+ * Batch mark read/unread and/or move folder for a list of ids under one inbox.
2173
+ * Unknown ids go in `failed`. Returns undefined when the inbox is unknown.
2174
+ */
2175
+ batchUpdateMessages(address: string, ids: string[], read: boolean | undefined, folder: string | undefined): BatchUpdateResult | undefined;
2176
+ /** Build all threads for an inbox, newest-active first. */
2177
+ private threadsFor;
2178
+ private buildThread;
2179
+ /**
2180
+ * Mock wait_for_email: synthesizes a plausible OTP email after a short delay so the OTP example
2181
+ * runs end-to-end offline. The live implementation waits through the platform receive path.
2182
+ */
2183
+ waitForEmail(address: string, req: WaitForEmailRequest): Promise<WaitForEmailResult>;
2184
+ /**
2185
+ * Append one event to the in-memory journal that backs the SSE stream. Mirrors
2186
+ * the server: a monotonic seq is the resume token and the payload is the same
2187
+ * envelope a webhook delivers.
2188
+ */
2189
+ private emitEvent;
2190
+ /**
2191
+ * Replay journal events after `lastEventId`, scoped to one inbox (or all when
2192
+ * `address` is null). The offline mock yields the backlog and returns; the live
2193
+ * transport tails an open connection. Both honor the same resume contract.
2194
+ */
2195
+ streamEvents(address: string | null, lastEventId?: number): AsyncGenerator<StreamEvent, void, unknown>;
2196
+ registerWebhook(req: RegisterWebhookRequest): Webhook;
2197
+ /** List registered webhooks (secret redacted, mirroring the server). */
2198
+ listWebhooks(): Page<Webhook>;
2199
+ /** Get one webhook by id (secret redacted), or undefined when missing. */
2200
+ getWebhook(webhookId: string): Webhook | undefined;
2201
+ /**
2202
+ * Update a webhook in place (secret redacted), or undefined when missing.
2203
+ * Every field is optional; an unset field leaves the stored value untouched
2204
+ * (PATCH semantics). An empty-string `inbox` clears the filter.
2205
+ */
2206
+ updateWebhook(webhookId: string, req: UpdateWebhookRequest): Webhook | undefined;
2207
+ /** Delete a webhook by id; returns false when it was not found. */
2208
+ deleteWebhook(webhookId: string): boolean;
2209
+ /** Add one allow/block entry scoped to an inbox, or undefined when the inbox is unknown. */
2210
+ addContactListEntry(address: string, req: AddContactListRequest): ContactListEntry | undefined;
2211
+ /** List the entries governing an inbox (inbox-specific + account-wide), or undefined when unknown. */
2212
+ listContactLists(address: string): Page<ContactListEntry> | undefined;
2213
+ /** Delete a contact-list entry by id; returns false when it was not found. */
2214
+ deleteContactListEntry(_address: string, entryId: string): boolean;
2215
+ /** Onboard a domain, mirroring the server's per-mode record set + status. Idempotent on the name. */
2216
+ onboardDomain(req: OnboardDomainRequest): Domain;
2217
+ /** List onboarded domains (records omitted on the summary, mirroring the server). */
2218
+ listDomains(): Page<Domain>;
2219
+ /** Get one domain's detail + the records to set, inline; undefined when absent. */
2220
+ getDomain(domain: string): Domain | undefined;
2221
+ /** Trigger/refresh verification; returns the (re-read) detail, or undefined when absent. */
2222
+ verifyDomain(domain: string): Domain | undefined;
2223
+ /**
2224
+ * Offboard (remove) a domain; returns false when it was not found. Also
2225
+ * records a synthetic succeeded teardown job under `job-offboard-<domain>`
2226
+ * (there is no job runner in the mock) so a subsequent {@link getJob} poll
2227
+ * resolves the same way the live transport's async contract would.
2228
+ */
2229
+ offboardDomain(domain: string): boolean;
2230
+ /** Get one async job's poll status; undefined when the id is unknown. */
2231
+ getJob(jobId: string): Job | undefined;
2232
+ /**
2233
+ * Pre-check whether the caller's org suppresses a recipient (mirrors
2234
+ * `GET /v1/suppressions?recipient=…`). Returns `{recipient, suppressed, rows}`
2235
+ * over the active (non-revoked) org rows for that canonicalized recipient.
2236
+ */
2237
+ precheckSuppression(recipient: string): SuppressionPrecheck;
2238
+ /** List the caller's own org suppression rows (mirrors the paged `GET /v1/suppressions`). */
2239
+ listSuppressions(params: ListSuppressionsParams): Page<SuppressionEntry>;
2240
+ /**
2241
+ * Revoke one org-scope suppression row (mirrors `POST /v1/suppressions/{id}/revoke`).
2242
+ * A reason is required (the caller validates too). Returns the updated row, or
2243
+ * undefined when the id is unknown / not the caller's own org row.
2244
+ */
2245
+ revokeSuppression(id: string, reason: string): SuppressionEntry | undefined;
2246
+ /**
2247
+ * Reject the WHOLE send if ANY recipient has an active org-scope suppression,
2248
+ * naming exactly the suppressed addresses (never the scope/origin) so the caller
2249
+ * can drop them and retry — mirroring the live `recipient_suppressed` (422) path.
2250
+ */
2251
+ private enforceSuppression;
2252
+ /**
2253
+ * Enforce the send-direction contact lists for an inbox: reject a block-listed
2254
+ * recipient, or any recipient outside the allowlist when allowlist mode is on.
2255
+ * Throws a {@link PermissionError} (403) to mirror the real API.
2256
+ */
2257
+ private enforceSendPolicy;
2258
+ }
2259
+
2260
+ /**
2261
+ * Key-tier awareness (redesign §3.1).
2262
+ *
2263
+ * An agent key encodes its CEILING tier in its raw prefix. The SDK never trusts
2264
+ * client input for scope — the tier is derived from the key the caller already
2265
+ * holds, purely as a client-side hint so an app can branch (e.g. an org-tier key
2266
+ * MUST pick a project breadth on a list; a project/inbox key may use the bare
2267
+ * sugar). The server remains the source of truth; this is advisory only.
2268
+ *
2269
+ * Prefix scheme (the secret tail is unchanged across tiers):
2270
+ * - `pk_agent_org_…` → {@link KeyTier.Org} (admin/console mint only)
2271
+ * - `pk_agent_proj_…` → {@link KeyTier.Project} (enrollment redeem + console)
2272
+ * - `pk_agent_inbox_…` → {@link KeyTier.Inbox} (admin/console mint only)
2273
+ * - legacy `pk_agent_…` (no tier segment) → {@link KeyTier.Project}
2274
+ */
2275
+ /** The ceiling tier encoded in an agent key's prefix. */
2276
+ type KeyTier = "org" | "project" | "inbox" | "unknown";
2277
+ /**
2278
+ * Derive the {@link KeyTier} from a raw agent key by peeking the segment after the
2279
+ * `pk_agent_` head. A legacy bare `pk_agent_…` key (no tier segment) maps to
2280
+ * `"project"` — exactly today's behavior. A non-agent credential (enrollment
2281
+ * token, Clerk session, empty) returns `"unknown"`.
2282
+ */
2283
+ declare function parseKeyTier(apiKey: string | undefined): KeyTier;
2284
+ /**
2285
+ * Whether a key of this tier can address the org-wide wildcard (`/v1/projects/-/…`).
2286
+ * Only an org-tier key may; a project/inbox key on the wildcard is a 403. Use this
2287
+ * to fail fast client-side before a round-trip.
2288
+ */
2289
+ declare function tierAllowsOrgWildcard(tier: KeyTier): boolean;
2290
+ /**
2291
+ * Whether a bare (project-less) list is unambiguous for this tier. An org-tier key
2292
+ * MUST pick a breadth (a concrete project or the `-` wildcard), so a bare list is a
2293
+ * 400 `breadth_required`; project/inbox keys default to their bound project.
2294
+ */
2295
+ declare function tierNeedsExplicitBreadth(tier: KeyTier): boolean;
2296
+
2297
+ /**
2298
+ * InboxHandle — an ergonomic, bound handle to a single inbox.
2299
+ *
2300
+ * Returned by `extrovert.inboxes.create(...)` and `extrovert.inbox(address)`, it scopes every operation
2301
+ * to one address so agent code reads naturally: `inbox.send(...)`, `inbox.waitForEmail(...)`. This
2302
+ * is the "one agent = one inbox" shape the spec's identity model encourages (§5).
2303
+ */
2304
+
2305
+ interface InboxHandleOptions {
2306
+ /** Default per-call timeout for blocking calls; resolved from the client config. */
2307
+ defaultWaitTimeoutMs: number;
2308
+ }
2309
+ declare class InboxHandle {
2310
+ private readonly transport;
2311
+ private readonly options;
2312
+ /** The canonical address, e.g. `agent7@smtp.extrovert.dev`. */
2313
+ readonly address: string;
2314
+ /** The full inbox record this handle was created from (absent when constructed by address). */
2315
+ readonly record: Inbox | undefined;
2316
+ constructor(transport: Transport, address: string, options: InboxHandleOptions, record?: Inbox);
2317
+ /** The inbox id, when known. */
2318
+ get id(): string | undefined;
2319
+ /**
2320
+ * The canonical key every transport op routes on. The contract canonicalizes the
2321
+ * OPAQUE inbox id (`pmbx_…`) and accepts the address only as a within-project alias
2322
+ * (both are valid in the `{inbox_id}` path slot). When this handle was built from a
2323
+ * full record we route by the canonical `id` (matching the "an inbox's canonical key
2324
+ * is its opaque id" docs); when built bare from an address (`client.inbox(addr)`),
2325
+ * the address alias is the only key we have, so we route on it.
2326
+ */
2327
+ private get ref();
2328
+ /**
2329
+ * Arbitrary key-value metadata on this inbox, when known (from the record this
2330
+ * handle was created from). Call {@link refresh} to re-read it after an update.
2331
+ */
2332
+ get metadata(): Inbox["metadata"] | undefined;
2333
+ /** Effective rolling-24-hour recipient cap, when known. */
2334
+ get dailySendLimit(): number | undefined;
2335
+ /** Provisioning status, when known. */
2336
+ get status(): InboxStatus | undefined;
2337
+ /** Onboarding mode of the host domain, when known. */
2338
+ get onboardingMode(): OnboardingMode | undefined;
2339
+ /** Credentials, present only when the inbox was created with `returnCredentials: true`. */
2340
+ get credentials(): InboxCredentials | undefined;
2341
+ /** Re-fetch the live inbox record. */
2342
+ refresh(signal?: AbortSignal): Promise<Inbox>;
2343
+ /**
2344
+ * Update this inbox's settings in place without delete+recreate: rename the sender
2345
+ * `display_name`, change the `webhook_url`, patch arbitrary `metadata` (shallow
2346
+ * merge; a key set to `null` deletes it, top-level `metadata: null` clears all),
2347
+ * or set `daily_send_limit` to 1–10,000 recipients per rolling 24h. The limit
2348
+ * field requires the opt-in `mailbox:quota` scope.
2349
+ * Returns the updated inbox record.
2350
+ */
2351
+ update(req: UpdateInboxRequest, signal?: AbortSignal): Promise<Inbox>;
2352
+ /**
2353
+ * Send an email from this authenticated inbox. Starts a new thread.
2354
+ *
2355
+ * Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
2356
+ * human has to approve it and NOTHING has been delivered yet; anything else was
2357
+ * delivered. Under the default `require_review` policy a call WITHOUT an
2358
+ * `intent` raises `IntentRequiredError` (422) instead — nothing sent, nothing
2359
+ * queued — so pass one, or read `inbox.record.effective_review_policy` first.
2360
+ */
2361
+ send(req: SendRequest, signal?: AbortSignal): Promise<SendOutcome>;
2362
+ /**
2363
+ * Reply within an existing thread. Select the parent with `thread_id` (reply to
2364
+ * the latest message) or `message_id` (reply to that message); the server
2365
+ * derives To / Subject / In-Reply-To / References. Set `reply_all` to reply to
2366
+ * every thread recipient. Returns the same three-way {@link SendOutcome} as
2367
+ * {@link send} — a reply is governed by the review policy too.
2368
+ */
2369
+ reply(req: ReplyRequest, signal?: AbortSignal): Promise<SendOutcome>;
2370
+ /**
2371
+ * Forward a message in this inbox to new recipients, preserving the original.
2372
+ *
2373
+ * A forward is an outbound message to arbitrary NEW recipients that quotes an
2374
+ * inbound thread, so it is governed by the review policy exactly like a send —
2375
+ * same {@link SendOutcome} union, same `intent` requirement.
2376
+ */
2377
+ forward(messageId: string, req: ForwardRequest, signal?: AbortSignal): Promise<SendOutcome>;
2378
+ /**
2379
+ * Submit a new message from this inbox for human review (Review Loop, HITL).
2380
+ * Pass `intent` (required when the resolved mode is review) and optionally
2381
+ * `mode`/`category_id`. The account/inbox review policy decides whether the
2382
+ * message is queued for review (`kind:"queued_for_review"`) or sent on a
2383
+ * policy-permitted direct path (`kind:"sent"`). Monitor it via
2384
+ * `extrovert.reviews`.
2385
+ */
2386
+ submitForReview(req: SendRequest, signal?: AbortSignal): Promise<SendOutcome>;
2387
+ /**
2388
+ * Submit an in-thread reply from this inbox for human review (Review Loop).
2389
+ * Same routing/return contract as {@link submitForReview}.
2390
+ */
2391
+ submitReplyForReview(req: ReplyRequest, signal?: AbortSignal): Promise<SendOutcome>;
2392
+ /**
2393
+ * List messages in this inbox, newest first. Narrow with exact-field filters
2394
+ * (from/to/subject substring) or `unread: true` (native \Seen); page with
2395
+ * limit + offset.
2396
+ */
2397
+ messages(params?: ListMessagesParams, signal?: AbortSignal): Promise<Page<Message>>;
2398
+ /** Full-text search this inbox (IMAP SEARCH over from/subject/body). */
2399
+ search(params: SearchMessagesParams, signal?: AbortSignal): Promise<Page<Message>>;
2400
+ /** Download the raw RFC822 `.eml` bytes for a message in this inbox. */
2401
+ messageRaw(messageId: string, signal?: AbortSignal): Promise<string>;
2402
+ /** Mark a message in this inbox read/unread via the native \Seen flag. */
2403
+ markRead(messageId: string, req: MarkReadRequest, signal?: AbortSignal): Promise<Message>;
2404
+ /**
2405
+ * Delete a message in this inbox: move it to Trash (default, recoverable) or
2406
+ * permanently remove it when `expunge` is true.
2407
+ */
2408
+ deleteMessage(messageId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
2409
+ /**
2410
+ * Batch mark read/unread and/or move folder for a list of message ids in this
2411
+ * inbox. At least one of `read` / `folder` must be set; returns the per-id
2412
+ * `{updated, failed}` split.
2413
+ */
2414
+ batchUpdateMessages(req: BatchUpdateMessagesRequest, signal?: AbortSignal): Promise<BatchUpdateResult>;
2415
+ /** List a message's attachment metadata ({id, filename, content_type, size}). */
2416
+ attachments(messageId: string, signal?: AbortSignal): Promise<Page<Attachment>>;
2417
+ /** Download one attachment's bytes (base64) plus filename and content type. */
2418
+ attachment(messageId: string, attachmentId: string, signal?: AbortSignal): Promise<AttachmentDownload>;
2419
+ /** List conversation threads in this inbox, newest-active first. */
2420
+ threads(params?: ListThreadsParams, signal?: AbortSignal): Promise<Page<Thread>>;
2421
+ /** Thread-level search in this inbox (subject / snippet / participants). */
2422
+ searchThreads(params: SearchMessagesParams, signal?: AbortSignal): Promise<Page<Thread>>;
2423
+ /** Fetch one thread (with its messages) in this inbox by stable id. */
2424
+ thread(threadId: string, signal?: AbortSignal): Promise<ThreadDetail>;
2425
+ /**
2426
+ * Delete an entire thread in this inbox (every message): move to Trash
2427
+ * (default) or permanently remove when `expunge` is true.
2428
+ */
2429
+ deleteThread(threadId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
2430
+ /**
2431
+ * Poll server-side until a matching message arrives and return it with an extracted
2432
+ * OTP / verification link. The HTTP read timeout is set a hair above the server-side
2433
+ * `timeoutSeconds` so the request doesn't abort before the server returns its own timeout result.
2434
+ */
2435
+ waitForEmail(req?: WaitForEmailRequest, signal?: AbortSignal): Promise<WaitForEmailResult>;
2436
+ /** Register an HMAC-signed inbound webhook scoped to this inbox. */
2437
+ registerWebhook(req: Omit<RegisterWebhookRequest, "inbox">, signal?: AbortSignal): Promise<Webhook>;
2438
+ /**
2439
+ * Add an allow/block contact-list entry to this inbox. A `block` entry rejects a
2440
+ * send to a matching recipient; once any `allow` entry exists, sends from this
2441
+ * inbox are restricted to recipients that match one (allowlist mode).
2442
+ */
2443
+ addContactListEntry(req: AddContactListRequest, signal?: AbortSignal): Promise<ContactListEntry>;
2444
+ /** List the allow/block contact-list entries governing this inbox. */
2445
+ listContactLists(signal?: AbortSignal): Promise<Page<ContactListEntry>>;
2446
+ /** Delete a contact-list entry on this inbox by id. */
2447
+ deleteContactListEntry(entryId: string, signal?: AbortSignal): Promise<void>;
2448
+ /**
2449
+ * Watch this inbox live (Server-Sent Events). Returns an async iterator of
2450
+ * {@link StreamEvent}; each `message.received` event arrives as it lands instead
2451
+ * of polling {@link waitForEmail}. Pass `lastEventId` (the `seq` of the last
2452
+ * event you saw) to resume without gaps after a reconnect, and a `signal` to
2453
+ * close the stream.
2454
+ *
2455
+ * ```ts
2456
+ * for await (const ev of inbox.stream()) {
2457
+ * if (ev.event === "message.received") console.log("new mail:", ev.message?.subject);
2458
+ * }
2459
+ * ```
2460
+ */
2461
+ stream(options?: StreamOptions): AsyncGenerator<StreamEvent, void, unknown>;
2462
+ /**
2463
+ * Convenience wrapper over {@link stream}: invoke `onEvent` for every event until
2464
+ * the stream closes (or `signal` aborts). Returns when the stream ends.
2465
+ */
2466
+ subscribe(onEvent: (event: StreamEvent) => void | Promise<void>, options?: StreamOptions): Promise<void>;
2467
+ /**
2468
+ * Permanently tear down this inbox and its messages/sender identity. Requires
2469
+ * `mailbox:delete`; this operation cannot be undone.
2470
+ */
2471
+ delete(signal?: AbortSignal): Promise<void>;
2472
+ }
2473
+
2474
+ /**
2475
+ * `extrovert.projects` — the CANONICAL project-scoped resource chain (redesign §4).
2476
+ *
2477
+ * Scope lives in the KEY; a broad (org-tier) key narrows to one project by PATH.
2478
+ * The headline chain is `x.projects.inboxes.*`, mirroring
2479
+ * `/v1/projects/{project_id}/inboxes[/{inbox_id}]`:
2480
+ *
2481
+ * ```ts
2482
+ * const inbox = await x.projects.inboxes.create("proj_9k", { username: "ada" });
2483
+ * const page = await x.projects.inboxes.list("proj_9k"); // List envelope
2484
+ * for await (const i of page) console.log(i.id); // auto-paginates
2485
+ * await x.projects.inboxes.send("proj_9k", inbox.id, { to, subject, text });
2486
+ * ```
2487
+ *
2488
+ * Operations are keyed by the OPAQUE `inbox_id` (the inbox's email address is also
2489
+ * accepted as a within-project alias). `projectId` may be `"-"` for the org-wide
2490
+ * wildcard — only an org-tier key may use it (others get 403 `forbidden_scope`).
2491
+ *
2492
+ * The bare `x.inboxes.*` / `x.inbox(address)` surface is curl-style sugar that
2493
+ * resolves to the key's default project; this chain is the contract-canonical one.
2494
+ */
2495
+
2496
+ /** Options forwarded into the chain (per-call wait timeout, resolved from the client). */
2497
+ interface ProjectsContext {
2498
+ transport: Transport;
2499
+ handleOptions: InboxHandleOptions;
2500
+ }
2501
+ /**
2502
+ * `x.projects.inboxes` — create / list / get / update / delete inboxes, plus the
2503
+ * send / reply / message / thread / wait operations, all scoped to one project (or
2504
+ * the `-` org wildcard for an org-tier key). List returns a {@link ListPage} that
2505
+ * auto-paginates over the opaque-cursor {@link import("../pagination.js").List} envelope.
2506
+ */
2507
+ declare class ProjectInboxes {
2508
+ private readonly ctx;
2509
+ constructor(ctx: ProjectsContext);
2510
+ /** Create an inbox in `projectId`. `idempotencyKey` (or `req.client_id`) makes it exactly-once. */
2511
+ create(projectId: string, req?: CreateInboxRequest, idempotencyKey?: string, signal?: AbortSignal): Promise<Inbox>;
2512
+ /**
2513
+ * List inboxes in `projectId` (or the org subtree with `projectId="-"`). Returns a
2514
+ * {@link ListPage} that yields the {@link import("../pagination.js").List} envelope's
2515
+ * rows and auto-paginates across pages via the opaque cursor.
2516
+ */
2517
+ list(projectId: string, params?: ProjectInboxListParams, signal?: AbortSignal): Promise<ListPage<Inbox>>;
2518
+ /** Get an inbox by opaque `inboxId` (or address alias) in `projectId`. */
2519
+ get(projectId: string, inboxId: string, params?: GetInboxParams, signal?: AbortSignal): Promise<Inbox>;
2520
+ /**
2521
+ * Update an inbox in place (display name / webhook / metadata / daily send
2522
+ * limit). `daily_send_limit` requires the opt-in `mailbox:quota` scope.
2523
+ */
2524
+ update(projectId: string, inboxId: string, req: UpdateInboxRequest, signal?: AbortSignal): Promise<Inbox>;
2525
+ /**
2526
+ * Permanently delete an inbox by opaque id (or address alias). Requires
2527
+ * `mailbox:delete`; the inbox and its messages cannot be recovered.
2528
+ */
2529
+ delete(projectId: string, inboxId: string, signal?: AbortSignal): Promise<void>;
2530
+ /** Fetch IMAP/SMTP connection settings + login for an inbox. */
2531
+ credentials(projectId: string, inboxId: string, signal?: AbortSignal): Promise<InboxCredentials>;
2532
+ /**
2533
+ * Send an email from an inbox in `projectId`.
2534
+ *
2535
+ * Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
2536
+ * human has to approve it and NOTHING has been delivered yet; anything else was
2537
+ * delivered. Under the default `require_review` policy a call WITHOUT an
2538
+ * `intent` raises `IntentRequiredError` (422) instead — nothing sent, nothing
2539
+ * queued — so pass one, or read `inbox.record.effective_review_policy` first.
2540
+ */
2541
+ send(projectId: string, inboxId: string, req: SendRequest, signal?: AbortSignal): Promise<SendOutcome>;
2542
+ /** Reply within a thread from an inbox in `projectId`. See {@link send} on the return union. */
2543
+ reply(projectId: string, inboxId: string, req: ReplyRequest, signal?: AbortSignal): Promise<SendOutcome>;
2544
+ /** Forward a message from an inbox in `projectId`. See {@link send} on the return union. */
2545
+ forward(projectId: string, inboxId: string, messageId: string, req: ForwardRequest, signal?: AbortSignal): Promise<SendOutcome>;
2546
+ /** List messages in an inbox in `projectId`. */
2547
+ messages(projectId: string, inboxId: string, params?: ListMessagesParams, signal?: AbortSignal): Promise<Page<Message>>;
2548
+ /** Search messages in an inbox in `projectId`. */
2549
+ searchMessages(projectId: string, inboxId: string, params: SearchMessagesParams, signal?: AbortSignal): Promise<Page<Message>>;
2550
+ /** Mark a message read/unread in an inbox in `projectId`. */
2551
+ markRead(projectId: string, inboxId: string, messageId: string, req: MarkReadRequest, signal?: AbortSignal): Promise<Message>;
2552
+ /** Delete a message in an inbox in `projectId`. */
2553
+ deleteMessage(projectId: string, inboxId: string, messageId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
2554
+ /** Batch mark/move messages in an inbox in `projectId`. */
2555
+ batchUpdateMessages(projectId: string, inboxId: string, req: BatchUpdateMessagesRequest, signal?: AbortSignal): Promise<BatchUpdateResult>;
2556
+ /** List a message's attachment metadata. */
2557
+ attachments(projectId: string, inboxId: string, messageId: string, signal?: AbortSignal): Promise<Page<Attachment>>;
2558
+ /** Download one attachment's bytes + filename + content type. */
2559
+ attachment(projectId: string, inboxId: string, messageId: string, attachmentId: string, signal?: AbortSignal): Promise<AttachmentDownload>;
2560
+ /** List conversation threads in an inbox in `projectId`. */
2561
+ threads(projectId: string, inboxId: string, params?: ListThreadsParams, signal?: AbortSignal): Promise<Page<Thread>>;
2562
+ /** Fetch one thread (+ its messages) in an inbox in `projectId`. */
2563
+ thread(projectId: string, inboxId: string, threadId: string, signal?: AbortSignal): Promise<ThreadDetail>;
2564
+ /**
2565
+ * Block until a matching message arrives in an inbox in `projectId` and return it
2566
+ * with an extracted OTP / verification link.
2567
+ */
2568
+ waitForEmail(projectId: string, inboxId: string, req?: WaitForEmailRequest, signal?: AbortSignal): Promise<WaitForEmailResult>;
2569
+ /**
2570
+ * The inbox reference the message/send/thread transport methods key on.
2571
+ *
2572
+ * The frozen contract project-prefixes ONLY the inbox collection/item/credentials
2573
+ * routes (`/v1/projects/{project_id}/inboxes[/{inbox_id}][/credentials]`); the
2574
+ * send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path —
2575
+ * they address the inbox by its opaque id directly (`/v1/inboxes/{inbox_id}/…`),
2576
+ * where the project is implicit in (and enforced by) the inbox id server-side.
2577
+ *
2578
+ * So for these sub-ops `projectId` cannot be carried on the URL and is NOT a URL
2579
+ * selector. The adversarial review flagged that silently discarding it makes the
2580
+ * signature misleading. CHOICE: keep the arg (dropping it would break the chain's
2581
+ * symmetry with create/list/get/update/delete — the more disruptive option) but
2582
+ * VALIDATE it rather than ignore it. We reject the two client mistakes we can catch
2583
+ * without a round-trip:
2584
+ * - a blank / whitespace-only `projectId` (a required selector everywhere else in
2585
+ * the chain), and
2586
+ * - the org wildcard `-`, which is meaningless for a single-inbox op (there is no
2587
+ * breadth to pick).
2588
+ * The inbox-id ↔ project binding itself is enforced server-side by the opaque id.
2589
+ */
2590
+ private ref;
2591
+ }
2592
+ /**
2593
+ * `extrovert.projects` — the canonical project-scoped resource namespace. Today it
2594
+ * exposes the `inboxes` chain (`x.projects.inboxes.*`); future project-scoped
2595
+ * resources (domains, agents) hang off the same namespace.
2596
+ */
2597
+ declare class Projects {
2598
+ /** `x.projects.inboxes.*` — the canonical inbox chain. */
2599
+ readonly inboxes: ProjectInboxes;
2600
+ constructor(ctx: ProjectsContext);
2601
+ }
2602
+
2603
+ /**
2604
+ * Resource namespaces: `extrovert.inboxes`, `extrovert.messages`, `extrovert.threads`,
2605
+ * `extrovert.webhooks`, `extrovert.contactLists`, `extrovert.domains`. Each is a thin,
2606
+ * typed facade over the {@link Transport}.
2607
+ */
2608
+
2609
+ interface ResourceContext {
2610
+ transport: Transport;
2611
+ handleOptions: InboxHandleOptions;
2612
+ /**
2613
+ * The CEILING tier derived from the configured key prefix. Advisory client-side
2614
+ * hint ONLY (the server stays authoritative); lets the bare sugar surface fail
2615
+ * fast for an org-tier key that must pick a breadth, matching the MCP surface.
2616
+ */
2617
+ keyTier: KeyTier;
2618
+ }
2619
+ /** `extrovert.inboxes` — create, list, get, update, delete inboxes. */
2620
+ declare class Inboxes {
2621
+ private readonly ctx;
2622
+ constructor(ctx: ResourceContext);
2623
+ /**
2624
+ * Create an inbox. The default path mints an address on a pre-verified shared subdomain of
2625
+ * `smtp.extrovert.dev`, so it returns a live, send-and-receive-capable inbox in one call.
2626
+ *
2627
+ * Pass `metadata` to attach arbitrary key-value data, and `client_id` for idempotent creation
2628
+ * (re-calling with the same id returns the same inbox, with its metadata replayed verbatim).
2629
+ */
2630
+ create(req?: CreateInboxRequest, signal?: AbortSignal): Promise<InboxHandle>;
2631
+ /**
2632
+ * List inboxes visible to the calling key (the bare curl-sugar surface — resolves
2633
+ * to the key's default project). An org-tier key has no single default project, so
2634
+ * the bare list is ambiguous: fail fast client-side with a BreadthRequiredError that
2635
+ * names the next call, matching the MCP surface, instead of round-tripping to a 400.
2636
+ * Use `extrovert.projects.inboxes.list("<project_id>")` or `"-"` (org subtree) for
2637
+ * an org key. The check is advisory — the server stays authoritative.
2638
+ */
2639
+ list(params?: ListInboxesParams, signal?: AbortSignal): Promise<Page<Inbox>>;
2640
+ /** Fetch a single inbox and return an ergonomic handle bound to it. */
2641
+ get(address: string, signal?: AbortSignal): Promise<InboxHandle>;
2642
+ /**
2643
+ * Update an inbox's settings in place without delete+recreate: rename the sender
2644
+ * `display_name`, change the `webhook_url`, patch arbitrary `metadata` (shallow
2645
+ * merge; a key set to `null` deletes it, top-level `metadata: null` clears all),
2646
+ * or set the effective `daily_send_limit` (1–10,000 recipients per rolling 24h).
2647
+ * Updating the daily limit requires the opt-in `mailbox:quota` scope.
2648
+ * Returns a handle bound to the updated record.
2649
+ */
2650
+ update(address: string, req: UpdateInboxRequest, signal?: AbortSignal): Promise<InboxHandle>;
2651
+ /**
2652
+ * Permanently delete an inbox by address. Requires `mailbox:delete`; the inbox,
2653
+ * messages, and sender identity cannot be recovered.
2654
+ */
2655
+ delete(address: string, signal?: AbortSignal): Promise<void>;
2656
+ }
2657
+ /**
2658
+ * `extrovert.messages` — read a message, fetch its raw bytes, mark it read.
2659
+ *
2660
+ * Reply and forward are inbox-scoped (the server resolves the parent and derives
2661
+ * recipients), so they live on the {@link InboxHandle} (`inbox.reply(...)`,
2662
+ * `inbox.forward(...)`), not here.
2663
+ */
2664
+ declare class Messages {
2665
+ private readonly ctx;
2666
+ constructor(ctx: ResourceContext);
2667
+ /** Fetch a single message by its opaque id; the owning inbox is resolved from the id. */
2668
+ get(messageId: string, signal?: AbortSignal): Promise<Message>;
2669
+ /**
2670
+ * Download the raw RFC822 `.eml` bytes for a message. `inbox` is the owning
2671
+ * address (needed to open the inbox); the id identifies the message.
2672
+ */
2673
+ raw(inbox: string, messageId: string, signal?: AbortSignal): Promise<string>;
2674
+ /**
2675
+ * Mark a message read/unread via the native IMAP \Seen flag (Extrovert's
2676
+ * label-free read state). `inbox` is the owning address.
2677
+ */
2678
+ markRead(inbox: string, messageId: string, req: MarkReadRequest, signal?: AbortSignal): Promise<Message>;
2679
+ /**
2680
+ * Delete a message: move it to Trash (default, recoverable) or permanently
2681
+ * remove it when `expunge` is true. `inbox` is the owning address.
2682
+ */
2683
+ delete(inbox: string, messageId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
2684
+ /**
2685
+ * Batch mark read/unread and/or move folder for a list of message ids in one
2686
+ * inbox. At least one of `read` / `folder` must be set; returns the per-id
2687
+ * `{updated, failed}` split.
2688
+ */
2689
+ batchUpdate(inbox: string, req: BatchUpdateMessagesRequest, signal?: AbortSignal): Promise<BatchUpdateResult>;
2690
+ /**
2691
+ * List a message's attachment metadata ({id, filename, content_type, size}).
2692
+ * `inbox` is the owning address; the message id identifies the message.
2693
+ */
2694
+ listAttachments(inbox: string, messageId: string, signal?: AbortSignal): Promise<Page<Attachment>>;
2695
+ /**
2696
+ * Download one attachment's bytes (base64) plus its filename and content type.
2697
+ * The "easy attachment fetch": `inbox` is the owning address, `messageId` the
2698
+ * message, and `attachmentId` the opaque id from {@link listAttachments}.
2699
+ */
2700
+ getAttachment(inbox: string, messageId: string, attachmentId: string, signal?: AbortSignal): Promise<AttachmentDownload>;
2701
+ }
2702
+ /**
2703
+ * `extrovert.threads` — fetch a conversation thread (with its messages) by id,
2704
+ * scoped to its owning inbox.
2705
+ */
2706
+ declare class Threads {
2707
+ private readonly ctx;
2708
+ constructor(ctx: ResourceContext);
2709
+ /** Fetch one thread (+ its messages, oldest-first) by id under its owning inbox address. */
2710
+ get(inbox: string, threadId: string, signal?: AbortSignal): Promise<ThreadDetail>;
2711
+ /**
2712
+ * Delete an entire thread (every message): move to Trash (default) or
2713
+ * permanently remove when `expunge` is true. `inbox` is the owning address.
2714
+ */
2715
+ delete(inbox: string, threadId: string, expunge?: boolean, signal?: AbortSignal): Promise<DeleteResult>;
2716
+ }
2717
+ /** `extrovert.webhooks` — register / list / get / update / delete HMAC-signed inbound webhooks. */
2718
+ declare class Webhooks {
2719
+ private readonly ctx;
2720
+ constructor(ctx: ResourceContext);
2721
+ /** Register an HMAC-signed, timestamped webhook. The signing `secret` is returned once. */
2722
+ register(req: RegisterWebhookRequest, signal?: AbortSignal): Promise<Webhook>;
2723
+ /** List registered webhooks (the one-time signing `secret` is omitted). */
2724
+ list(signal?: AbortSignal): Promise<Page<Webhook>>;
2725
+ /** Fetch one webhook by id (the signing `secret` is omitted). */
2726
+ get(webhookId: string, signal?: AbortSignal): Promise<Webhook>;
2727
+ /**
2728
+ * Update a webhook in place: change the delivery `url`, the subscribed
2729
+ * `events`, the `inbox` filter (empty string clears it), or `active` to
2730
+ * enable/disable delivery. Omitted fields are left unchanged (PATCH
2731
+ * semantics). The signing `secret` is immutable and stays redacted.
2732
+ */
2733
+ update(webhookId: string, req: UpdateWebhookRequest, signal?: AbortSignal): Promise<Webhook>;
2734
+ /** Delete a webhook by id. */
2735
+ delete(webhookId: string, signal?: AbortSignal): Promise<void>;
2736
+ }
2737
+ /**
2738
+ * `extrovert.contactLists` — per-inbox allow/block lists of addresses/domains.
2739
+ * A `block` entry rejects a send to a matching recipient; once an `allow` entry
2740
+ * exists for an inbox, sends from it are restricted to matching recipients
2741
+ * (allowlist mode). Entries are addressable by their opaque id (`lst_…`).
2742
+ */
2743
+ declare class ContactLists {
2744
+ private readonly ctx;
2745
+ constructor(ctx: ResourceContext);
2746
+ /** Add an allow/block entry to an inbox. */
2747
+ add(inbox: string, req: AddContactListRequest, signal?: AbortSignal): Promise<ContactListEntry>;
2748
+ /** List the entries governing an inbox (inbox-specific + account-wide). */
2749
+ list(inbox: string, signal?: AbortSignal): Promise<Page<ContactListEntry>>;
2750
+ /** Delete an entry on an inbox by id. */
2751
+ delete(inbox: string, entryId: string, signal?: AbortSignal): Promise<void>;
2752
+ }
2753
+ /**
2754
+ * `extrovert.suppressions` — recipient opt-outs (list-unsubscribe). A recipient
2755
+ * that has unsubscribed cannot be mailed by this org: a send to them is rejected
2756
+ * with `recipient_suppressed` ({@link RecipientSuppressedError}). Use `precheck`
2757
+ * before composing to skip a would-be-rejected recipient, `list` to browse the
2758
+ * org's opt-outs, and `revoke` (reason required, audit-logged) to re-enable a
2759
+ * recipient. All reads/writes are scoped to the caller's OWN org — a
2760
+ * platform-global or shared-domain opt-out is never surfaced here.
2761
+ */
2762
+ declare class Suppressions {
2763
+ private readonly ctx;
2764
+ constructor(ctx: ResourceContext);
2765
+ /**
2766
+ * Pre-check whether the caller's org already suppresses a recipient, BEFORE
2767
+ * composing. `suppressed: true` means a send to them would be rejected — skip
2768
+ * that recipient. Returns the matching org rows too (never a global/shared row).
2769
+ */
2770
+ precheck(recipient: string, signal?: AbortSignal): Promise<SuppressionPrecheck>;
2771
+ /** List the org's suppression rows (active by default; `include_revoked` for all). */
2772
+ list(params?: ListSuppressionsParams, signal?: AbortSignal): Promise<Page<SuppressionEntry>>;
2773
+ /**
2774
+ * Revoke one org-scope suppression row (re-enable sending to that recipient). A
2775
+ * `reason` is REQUIRED (empty/whitespace is a 400) and is audit-logged. A
2776
+ * foreign/global/shared id is an indistinguishable 404.
2777
+ */
2778
+ revoke(id: string, reason: string, signal?: AbortSignal): Promise<SuppressionEntry>;
2779
+ }
2780
+ /**
2781
+ * `extrovert.domains` — the customer's domains (privileged; the agent key must
2782
+ * carry the `domain:manage` scope). Onboard (shared | ns_delegated | manual |
2783
+ * purchased), read status + the DNS records to set inline, trigger/refresh
2784
+ * verification, and offboard. `mode: "purchased"` spends money at the registrar and
2785
+ * ADDITIONALLY requires the explicit, default-off `domain:purchase` scope (and is
2786
+ * capped by the org/project purchased-domain plan limit). Set `scope: "project"` to
2787
+ * bind the domain to the key's project; it defaults to `org` (org-shared).
2788
+ */
2789
+ declare class Domains {
2790
+ private readonly ctx;
2791
+ constructor(ctx: ResourceContext);
2792
+ /** List the customer's onboarded domains and their status. */
2793
+ list(signal?: AbortSignal): Promise<Page<Domain>>;
2794
+ /** Get one domain's detail + verification status + the DNS records to set, inline. */
2795
+ get(domain: string, signal?: AbortSignal): Promise<Domain>;
2796
+ /**
2797
+ * Onboard (add) a domain. `mode` defaults to ns_delegated. `mode: "purchased"`
2798
+ * requires the `domain:purchase` scope (in addition to `domain:manage`). Returns
2799
+ * the record set / NS instruction.
2800
+ */
2801
+ onboard(req: OnboardDomainRequest, signal?: AbortSignal): Promise<Domain>;
2802
+ /** Trigger or refresh verification for a domain; returns its (possibly advanced) status. */
2803
+ verify(domain: string, signal?: AbortSignal): Promise<Domain>;
2804
+ /**
2805
+ * Offboard (remove) a domain from the customer. Async: the API accepts the
2806
+ * request (HTTP 202) and tears the domain down as a job. Returns the accepted
2807
+ * job's id + poll URL (`status_url`); poll it with `extrovert.getJob(job_id)`
2808
+ * until the status is terminal (succeeded/failed/cancelled).
2809
+ */
2810
+ offboard(domain: string, signal?: AbortSignal): Promise<DomainOffboard>;
2811
+ }
2812
+ /**
2813
+ * `extrovert.reviews` — the Review Loop (HITL) agent-plane reads. A sending agent
2814
+ * monitors its submissions in the human-review queue: list/get a review request and
2815
+ * read its append-only thread of turns (intent, drafts, human comments/edits/
2816
+ * decisions, captured diffs). Submitting FOR review rides `inbox.send` /
2817
+ * `inbox.reply` with `mode`/`intent`/`category_id`. Human-authority actions
2818
+ * (approve/reject/edit-send) are console-only (D17) and never exposed here.
2819
+ */
2820
+ declare class Reviews {
2821
+ private readonly ctx;
2822
+ /**
2823
+ * `extrovert.reviews.events` — the Review Loop (HITL) realtime plane: drain,
2824
+ * long-poll, and ack the durable nudge queue (the AUTHORITATIVE liveness source;
2825
+ * SSE/webhook are best-effort fast paths on top of it).
2826
+ */
2827
+ readonly events: ReviewEvents;
2828
+ constructor(ctx: ResourceContext);
2829
+ /** List review requests (customer-scoped). Filter by state / category / inbox. */
2830
+ list(params?: ListReviewsParams, signal?: AbortSignal): Promise<Page<Review>>;
2831
+ /** Get one review request by id (rr_…): current draft + intent + state. */
2832
+ get(reviewId: string, signal?: AbortSignal): Promise<Review>;
2833
+ /** Get a review's append-only thread turns by id (rr_…). */
2834
+ turns(reviewId: string, signal?: AbortSignal): Promise<Page<ReviewTurn>>;
2835
+ /**
2836
+ * Get the human's assembled feedback (M5): the diff + comments + decision + the
2837
+ * rules born from this review. Read it after a rejected/edited nudge to learn what
2838
+ * the human wanted. $0 LLM — pure assembly on our side.
2839
+ */
2840
+ feedback(reviewId: string, signal?: AbortSignal): Promise<ReviewFeedback>;
2841
+ /**
2842
+ * Post a chat turn on a review's thread (M5): an agent question to the human
2843
+ * reviewer; flips in_review -> chatting on the first turn. Idempotent on the
2844
+ * optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM — you compose it.
2845
+ */
2846
+ chat(reviewId: string, req: PostReviewChatRequest, idempotencyKey?: string, signal?: AbortSignal): Promise<Review>;
2847
+ /**
2848
+ * Post a new agent draft under a parent_revision CAS (M5; D17). parent_revision
2849
+ * must equal the draft's current revision, else a 409 STALE with NO mutation (the
2850
+ * human always wins — re-read, re-apply, retry). On success the draft is re-rendered
2851
+ * in place (revision++) and returns to needs_review. $0 LLM — you compose the redraft.
2852
+ */
2853
+ revise(reviewId: string, req: SubmitRevisionRequest, signal?: AbortSignal): Promise<Review>;
2854
+ /**
2855
+ * Withdraw your own pending review (M5) to the terminal cancelled state. Only the
2856
+ * composing agent may cancel; a terminal (already sent) review 409s.
2857
+ */
2858
+ cancel(reviewId: string, idempotencyKeyOrSignal?: string | AbortSignal, signal?: AbortSignal): Promise<Review>;
2859
+ /**
2860
+ * Re-stamp a draft's rules-version WITHOUT redrafting (M7; D19/§8 $0 escape valve):
2861
+ * assert "I reviewed this against rules vX and no change is needed", advancing the
2862
+ * draft's composed_* versions with no new draft, no revision bump, no nudge. A
2863
+ * born-stale draft re-stamped to the current version becomes current-enough and
2864
+ * releasable on the next reconciliation sweep — the cheap counterpart to revise().
2865
+ * against_version above the category's current rules-version is 400; a terminal draft
2866
+ * 409s. $0 LLM — you judged.
2867
+ */
2868
+ restamp(reviewId: string, req: RestampReviewRequest, signal?: AbortSignal): Promise<Review>;
2869
+ /**
2870
+ * Get the REVIEWER's decision context for a review (M8 Slice B; D5/§9). The reviewer
2871
+ * is an AGENT granted review:act, authorized for THIS review ONLY via a matching
2872
+ * ACTIVE review-link (per-inbox beats account-wide). The context is the intent +
2873
+ * current draft + thread + the two-circuit-breaker budget (hop_count vs max_hops, the
2874
+ * hard review_deadline). `force_to_human` is true when a reject would be FORCED to the
2875
+ * human regardless of intent (the human is the only terminal authority, D17). A
2876
+ * cross-tenant id is 404; a non-reviewer is 403. Read-only, $0 LLM.
2877
+ */
2878
+ decisionContext(reviewId: string, signal?: AbortSignal): Promise<ReviewDecisionContext>;
2879
+ /**
2880
+ * Submit a reviewer decision (M8 Slice B; reviewer_decide, D5/§9). approve/edit → the
2881
+ * PLATFORM ACS-sends with the COMPOSER's credentials (the reviewer NEVER holds
2882
+ * mailbox:send on an inbox it doesn't own — the credential boundary); reject → back to
2883
+ * the composer (needs_review, hop_count++); escalate → the human queue. revision/
2884
+ * version are the CAS (409 STALE on mismatch, NO mutation — the human always wins,
2885
+ * D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline)
2886
+ * FORCE a reject to the human regardless of intent — `forced_by_breaker` names it. $0
2887
+ * LLM — you judged; we route, send, and enforce the breakers.
2888
+ */
2889
+ decide(reviewId: string, req: ReviewerDecisionRequest, signal?: AbortSignal): Promise<ReviewerDecisionResult>;
2890
+ }
2891
+ /**
2892
+ * `extrovert.reviews.events` — drain / long-poll / ack the durable review nudge
2893
+ * queue (spec §5.9). `list` is a non-blocking, side-effect-free drain of the next
2894
+ * un-acked nudges in FIFO seq order (strict per review); `wait` long-polls
2895
+ * (~25–55s) for the next one; `ack` advances the per-(agent, review) cursor
2896
+ * monotonically (idempotent — re-acking an older seq is a no-op).
2897
+ */
2898
+ declare class ReviewEvents {
2899
+ private readonly ctx;
2900
+ constructor(ctx: ResourceContext);
2901
+ /** Drain the next un-acked review events (non-blocking). */
2902
+ list(params?: ListReviewEventsParams, signal?: AbortSignal): Promise<ReviewEventsResult>;
2903
+ /** Long-poll for the next review event (empty on timeout). */
2904
+ wait(params?: WaitForReviewEventParams, signal?: AbortSignal): Promise<ReviewEventsResult>;
2905
+ /** Advance the per-review cursor(s) and/or mark broadcast nudges done. */
2906
+ ack(req: AckReviewEventRequest, signal?: AbortSignal): Promise<AckReviewEventResult>;
2907
+ }
2908
+ /**
2909
+ * `extrovert.categories` — the Review Loop category registry (D9/D10). Browse and
2910
+ * MATCH an existing category before composing (like a skills registry), or propose
2911
+ * a new one. Categories are CUSTOMER-scoped and agent-attributed (the deliberate
2912
+ * cross-agent-404 exception); identity is opaque cat_ ids — nothing keys on the
2913
+ * name, so renames never break a reference. `match` is a pure lexical filter (NO
2914
+ * LLM on our side); the agent does the semantic matching. Merging / deleting a
2915
+ * category is a human (console) action, not exposed here (D17).
2916
+ */
2917
+ declare class Categories {
2918
+ private readonly ctx;
2919
+ constructor(ctx: ResourceContext);
2920
+ /** Browse the registry. `match` lexically filters name+description (NO LLM). */
2921
+ list(params?: ListCategoriesParams, signal?: AbortSignal): Promise<Page<Category>>;
2922
+ /** Get one category by id (cat_…): name + description + scope + state. */
2923
+ get(categoryId: string, signal?: AbortSignal): Promise<Category>;
2924
+ /** Propose a new category; it stands immediately and writes a create audit row. */
2925
+ propose(req: ProposeCategoryRequest, signal?: AbortSignal): Promise<Category>;
2926
+ /** Rename / re-describe a category — metadata only (D10). */
2927
+ update(categoryId: string, req: UpdateCategoryRequest, signal?: AbortSignal): Promise<Category>;
2928
+ /**
2929
+ * Read the effective risk dial (D4/D12): the account default + every category's
2930
+ * overrides (each with its resolved effective value; null override = inherit).
2931
+ * Read-only — agents read but NEVER flip the dial; setting it is a human (console)
2932
+ * action (D16).
2933
+ */
2934
+ riskDial(signal?: AbortSignal): Promise<RiskDial>;
2935
+ /**
2936
+ * Read a category's graduation gate status (D16): the gates passed / still needed
2937
+ * toward the next rung (approvals N/needed, age, maturity gate, drift vs K,
2938
+ * can_graduate). Read-only.
2939
+ */
2940
+ graduationStatus(categoryId: string, signal?: AbortSignal): Promise<GraduationStatus>;
2941
+ /**
2942
+ * Propose graduating a category (D16/D6): RECORDS the request (durable evidence) and
2943
+ * returns the current gate status. It does NOT change the category state — flipping
2944
+ * the bit is a human (console) action; an agent only proposes.
2945
+ */
2946
+ proposeGraduation(categoryId: string, req?: ProposeGraduationRequest, signal?: AbortSignal): Promise<GraduationStatus>;
2947
+ /**
2948
+ * Read the D19/§8 backlog-reconciliation status: how many of the category's QUEUED
2949
+ * drafts are stale vs current-enough against the current rules-version (a pure
2950
+ * integer compare, $0 LLM). Read-only — you READ the picture; the human (console
2951
+ * scan-backlog) or the graduate/rule-change hooks TRIGGER the actual reconciliation
2952
+ * sweep that releases current-enough drafts and nudges stale ones to redraft.
2953
+ */
2954
+ backlogStatus(categoryId: string, signal?: AbortSignal): Promise<ScanBacklogStatus>;
2955
+ /**
2956
+ * Read the demand-driven pacing state (M7 Slice B/§8): the human review cursor, the
2957
+ * effective lookahead window (freshness is guaranteed only for the next few drafts
2958
+ * after the cursor), the HARD per-nudge fan-out ceiling (rework_batch_max), the
2959
+ * per-agent nudge interval, and each queued draft's classification (behind_cursor |
2960
+ * in_window_fresh | in_window_redrafting | ahead). Read-only; the cursor advances
2961
+ * from the human's console approve/reject/edit actions.
2962
+ */
2963
+ pacingState(categoryId: string, signal?: AbortSignal): Promise<CategoryPacingState>;
2964
+ }
2965
+ /**
2966
+ * `extrovert.rules` — the Review Loop writing-rule store + house-style + the §7
2967
+ * precedence ladder + audit/undo (D2/D11). ANY agent in the customer may write,
2968
+ * edit, promote, retire, and undo rules (the deliberate cross-agent exception — the
2969
+ * shared house-style is the whole pitch). `get()` returns the ORDERED active rule
2970
+ * set with the precedence ladder applied SERVER-SIDE (NO LLM on our side); the agent
2971
+ * reconciles the list semantically. Rules are append-only by supersession; undo
2972
+ * restores the prior version as a forward 'restore' supersession. Identity is opaque
2973
+ * rule_/rln_/udo_ ids — nothing keys on a name.
2974
+ */
2975
+ declare class Rules {
2976
+ private readonly ctx;
2977
+ constructor(ctx: ResourceContext);
2978
+ /** Get the ORDERED active rule set (precedence ladder applied; NO LLM). */
2979
+ get(params?: GetRulesParams, signal?: AbortSignal): Promise<Page<Rule>>;
2980
+ /**
2981
+ * Save / edit a rule (append-only by supersession; D11). An agent-plane save is
2982
+ * ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
2983
+ * key's project. Agents cannot author org-layer / house-style (`rule_layer:"org"`)
2984
+ * rules in v1 — that is a console/admin action.
2985
+ */
2986
+ save(req: SaveRuleRequest, signal?: AbortSignal): Promise<Rule>;
2987
+ /** Promote a rule between the category and general/house-style layers. */
2988
+ promote(ruleId: string, toScope: "general" | "category", signal?: AbortSignal): Promise<Rule>;
2989
+ /** Retire a rule — soft delete; the history survives as training data. */
2990
+ retire(ruleId: string, signal?: AbortSignal): Promise<Rule>;
2991
+ /** Read the rule/category change audit log (the safety net, D11). */
2992
+ audit(params?: GetRuleAuditParams, signal?: AbortSignal): Promise<Page<RuleAuditEntry>>;
2993
+ /** Undo a rule change by its audit-row id (udo_…) — restore the prior version. */
2994
+ undo(udoId: string, signal?: AbortSignal): Promise<Rule>;
2995
+ }
2996
+
2997
+ /**
2998
+ * ExtrovertClient — the entry point.
2999
+ *
3000
+ * ```ts
3001
+ * import { Extrovert } from "@extrovert.dev/sdk";
3002
+ * const extrovert = new Extrovert({ apiKey: process.env.EXTROVERT_API_KEY! });
3003
+ * const inbox = await extrovert.inboxes.create(); // a real inbox, in one call
3004
+ * const outcome = await inbox.send({
3005
+ * to: "ops@acme.test",
3006
+ * subject: "hi",
3007
+ * text: "from an agent",
3008
+ * intent: { summary: "send the requested status update" },
3009
+ * });
3010
+ * if (outcome.kind === "queued_for_review") console.log(outcome.review.id);
3011
+ * ```
3012
+ */
3013
+
3014
+ /** Default production API base URL. Overridable via `baseUrl` or `EXTROVERT_API_BASE_URL`. */
3015
+ declare const DEFAULT_BASE_URL = "https://api.extrovert.dev";
3016
+ /** The sentinel that routes the client to the offline mock instead of the network. */
3017
+ declare const MOCK_BASE_URL = "mock";
3018
+ interface ExtrovertClientOptions {
3019
+ /**
3020
+ * Scoped agent key (`pk_agent_...`) or, for `enroll`, any bearer credential the server accepts.
3021
+ * Falls back to `EXTROVERT_API_KEY` when omitted.
3022
+ */
3023
+ apiKey?: string;
3024
+ /**
3025
+ * API base URL. Falls back to `EXTROVERT_API_BASE_URL`, then {@link DEFAULT_BASE_URL}. Set to
3026
+ * `"mock"` (or `transport: "mock"`) to run fully offline against the built-in fixtures.
3027
+ */
3028
+ baseUrl?: string;
3029
+ /** Force a transport. `"mock"` ignores `baseUrl` and never touches the network. */
3030
+ transport?: "http" | "mock";
3031
+ /** Default request timeout in ms. Default 30000. (wait_for_email manages its own, longer timeout.) */
3032
+ timeoutMs?: number;
3033
+ /** Retry policy for idempotent requests on 429/5xx/network errors. */
3034
+ retry?: Partial<RetryOptions>;
3035
+ /** Custom fetch implementation (tests, proxies, instrumentation). Defaults to global `fetch`. */
3036
+ fetch?: typeof fetch;
3037
+ /** Extra headers merged into every request. */
3038
+ defaultHeaders?: Record<string, string>;
3039
+ /**
3040
+ * Dated API version to pin (sent as the `Extrovert-Version` header on every
3041
+ * request; redesign §5.4). Defaults to {@link CURRENT_API_VERSION}. Pin an older
3042
+ * dated version to opt into the server's transform shim for that version.
3043
+ */
3044
+ apiVersion?: string;
3045
+ /** Inject a pre-built mock backend (shares state across clients in tests). */
3046
+ mockBackend?: MockBackend;
3047
+ }
3048
+ declare class ExtrovertClient {
3049
+ /** `extrovert.inboxes` — create / list / get / update / delete inboxes. */
3050
+ readonly inboxes: Inboxes;
3051
+ /** `extrovert.messages` — read a message, reply to it (threaded). */
3052
+ readonly messages: Messages;
3053
+ /** `extrovert.threads` — fetch a conversation thread. */
3054
+ readonly threads: Threads;
3055
+ /** `extrovert.webhooks` — register HMAC-signed inbound webhooks. */
3056
+ readonly webhooks: Webhooks;
3057
+ /** `extrovert.contactLists` — per-inbox allow/block lists of addresses/domains. */
3058
+ readonly contactLists: ContactLists;
3059
+ /** `extrovert.suppressions` — recipient opt-outs (list-unsubscribe); precheck/list/revoke. */
3060
+ readonly suppressions: Suppressions;
3061
+ /** `extrovert.domains` — the customer's domains (privileged; domain:manage scope). */
3062
+ readonly domains: Domains;
3063
+ /** `extrovert.reviews` — the Review Loop (HITL) agent-plane reads. */
3064
+ readonly reviews: Reviews;
3065
+ /** `extrovert.categories` — the Review Loop category registry (browse/propose/curate). */
3066
+ readonly categories: Categories;
3067
+ /** `extrovert.rules` — the Review Loop writing-rule store + house-style + audit/undo. */
3068
+ readonly rules: Rules;
3069
+ /**
3070
+ * `extrovert.projects` — the CANONICAL project-scoped chain. The headline is
3071
+ * `extrovert.projects.inboxes.*` (create/list/get/update/delete/send/...), keyed by
3072
+ * the opaque `inbox_id` and scoped to a `{project_id}` path (or `-` for the org
3073
+ * wildcard on an org-tier key). The bare `extrovert.inboxes` surface is curl sugar
3074
+ * that resolves to the key's default project.
3075
+ */
3076
+ readonly projects: Projects;
3077
+ /** The resolved API base URL (or `"mock"`). */
3078
+ readonly baseUrl: string;
3079
+ /**
3080
+ * The dated API version pinned on every request (`Extrovert-Version`). Defaults to
3081
+ * {@link CURRENT_API_VERSION}.
3082
+ */
3083
+ readonly apiVersion: string;
3084
+ /**
3085
+ * The CEILING tier derived from the configured agent key prefix (`org` | `project`
3086
+ * | `inbox` | `unknown`). Advisory client-side hint only — the server is the source
3087
+ * of truth. Lets an app branch (e.g. require a project pick for an org-tier key).
3088
+ */
3089
+ readonly keyTier: KeyTier;
3090
+ private readonly transport;
3091
+ private readonly handleOptions;
3092
+ constructor(options?: ExtrovertClientOptions);
3093
+ /**
3094
+ * Redeem an enrollment token (`pk_enroll_...`) and mint a scoped agent key.
3095
+ *
3096
+ * Idempotent on `agent_handle`: redeeming twice with the same handle returns the same agent.
3097
+ * Returns the raw `EnrollResponse` — to immediately use the minted key, prefer
3098
+ * {@link ExtrovertClient.enrolled}.
3099
+ */
3100
+ enroll(req: EnrollRequest, signal?: AbortSignal): Promise<EnrollResponse>;
3101
+ /**
3102
+ * Grab a free account in one unauthenticated call (Slice E). Provisions a tenant
3103
+ * + a first inbox and returns a LIMITED-scope (read-only) agent key; a one-time
3104
+ * code is emailed to `human_email`. Call {@link verify} with the code to unlock
3105
+ * full scopes. Idempotent on `human_email`: re-calling rotates the key + resends.
3106
+ */
3107
+ signUp(req: SignUpRequest, signal?: AbortSignal): Promise<SignUpResponse>;
3108
+ /**
3109
+ * Confirm the emailed signup code and receive a NEW full-scope agent key (shown
3110
+ * once). Must be called with the limited key from {@link signUp} as the bearer.
3111
+ */
3112
+ verify(req: VerifyRequest, signal?: AbortSignal): Promise<VerifyResponse>;
3113
+ /** Introspect the principal behind the current key (`GET /v1/auth/me`). */
3114
+ whoami(signal?: AbortSignal): Promise<WhoAmI>;
3115
+ /**
3116
+ * Poll the status of an async job (`GET /v1/jobs/{job_id}`) — currently only
3117
+ * the domain-offboard teardown started by {@link Domains.offboard} enqueues
3118
+ * one. `status` is terminal on succeeded/failed/cancelled; keep polling
3119
+ * otherwise. An unknown or foreign job id is a {@link NotFoundError}.
3120
+ */
3121
+ getJob(jobId: string, signal?: AbortSignal): Promise<Job>;
3122
+ /**
3123
+ * Redeem an enrollment token and return a *new* client already authenticated with the minted
3124
+ * agent key — the natural "redeem then act" flow for an agent.
3125
+ *
3126
+ * ```ts
3127
+ * const bootstrap = new Extrovert({ apiKey: enrollmentToken });
3128
+ * const { client, enrollment } = await bootstrap.enrolled({
3129
+ * token: enrollmentToken,
3130
+ * agent_handle: "support-bot",
3131
+ * });
3132
+ * const inbox = await client.inboxes.create();
3133
+ * ```
3134
+ */
3135
+ enrolled(req: EnrollRequest, options?: Pick<ExtrovertClientOptions, "timeoutMs" | "retry" | "fetch" | "defaultHeaders">, signal?: AbortSignal): Promise<{
3136
+ client: ExtrovertClient;
3137
+ enrollment: EnrollResponse;
3138
+ }>;
3139
+ /**
3140
+ * Get an ergonomic handle to an existing inbox by address — without an extra round-trip. Use this
3141
+ * when you already know the address (e.g. from a previous create) and want to send/wait/reply.
3142
+ * Call {@link InboxHandle.refresh} to load the full record.
3143
+ */
3144
+ inbox(address: string): InboxHandle;
3145
+ /**
3146
+ * The headline shortcut: provision a real inbox in one call and get a handle bound to it.
3147
+ * Equivalent to `extrovert.inboxes.create(req)`, named to match the docs/marketing promise.
3148
+ */
3149
+ createInbox(req?: CreateInboxRequest, signal?: AbortSignal): Promise<InboxHandle>;
3150
+ /**
3151
+ * Watch events across EVERY inbox the agent owns (Server-Sent Events, `GET
3152
+ * /v1/events`). Returns an async iterator of {@link StreamEvent}; pass
3153
+ * `lastEventId` (the `seq` of the last event you saw) to resume after a reconnect
3154
+ * and a `signal` to close the stream. To watch a single inbox, use
3155
+ * {@link InboxHandle.stream} instead.
3156
+ *
3157
+ * ```ts
3158
+ * for await (const ev of extrovert.stream()) {
3159
+ * if (ev.event === "message.received") console.log(ev.inbox, ev.message?.subject);
3160
+ * }
3161
+ * ```
3162
+ */
3163
+ stream(options?: StreamOptions): AsyncGenerator<StreamEvent, void, unknown>;
3164
+ /**
3165
+ * Convenience wrapper over {@link stream}: invoke `onEvent` for every event
3166
+ * across all owned inboxes until the stream closes (or `signal` aborts).
3167
+ */
3168
+ subscribe(onEvent: (event: StreamEvent) => void | Promise<void>, options?: StreamOptions): Promise<void>;
3169
+ }
3170
+
3171
+ /**
3172
+ * RFC-9457 problem+json errors (redesign §5.1 / §6.2).
3173
+ *
3174
+ * The redesigned surface returns `application/problem+json` with a CLOSED machine
3175
+ * `code` enum clients switch on. The SDK parses it into {@link Problem} and raises a
3176
+ * {@link ProblemError} whose `code` is the typed {@link ProblemCode} union, so callers
3177
+ * branch exhaustively without inspecting raw bodies. The legacy `{ error, message }`
3178
+ * envelope is still parsed by the transport (back-compat); when present it is mapped
3179
+ * onto the closest {@link ProblemCode}.
3180
+ */
3181
+ /**
3182
+ * The CLOSED problem code enum (mirrors `components.schemas.Problem.code` in the
3183
+ * frozen OpenAPI). Adding a member is a contract change — keep it in lockstep with
3184
+ * the Go `ProblemCode` enum.
3185
+ */
3186
+ type ProblemCode = "bad_request" | "unauthorized" | "forbidden_scope" | "not_found" | "conflict" | "idempotency_conflict" | "breadth_required" | "quota_exceeded" | "rate_limited" | "domain_not_allowed" | "recipient_blocked" | "recipient_suppressed" | "not_configured" | "domain_unavailable" | "internal" | "intent_required" | "wrong_state" | "terminal" | "stale" | "born_stale" | "send_needs_reconciliation" | "graduation_locked" | "maturity_gate_unmet" | "scope_taken" | "unavailable";
3187
+ /** The full set of {@link ProblemCode} values (for runtime validation / exhaustive UIs). */
3188
+ declare const PROBLEM_CODES: readonly ProblemCode[];
3189
+ /**
3190
+ * Which review-loop problem codes are worth retrying, and which are a dead end.
3191
+ *
3192
+ * This table is the whole point of splitting the 409: only a failed
3193
+ * compare-and-set (`stale`) and a redraft built against an older rule high-water
3194
+ * (`born_stale`) describe a situation a retry can fix, and each only a bounded
3195
+ * number of times (re-read, re-apply on top of the other party's change,
3196
+ * resubmit). `wrong_state` means the verb is wrong, not the timing — read the
3197
+ * `allowed_action` hints and pick another one. `terminal` means the review is
3198
+ * finished forever; a `front_run_next` nudge is already waiting on the queue
3199
+ * with the outcome. `send_needs_reconciliation` means a delivery attempt is
3200
+ * unconfirmed — resending is precisely how a message goes out twice.
3201
+ *
3202
+ * `intent_required` is listed false because retrying the SAME bytes fails
3203
+ * identically: the fix is to ADD an `intent` and send a different request. The
3204
+ * server states in its `detail` that nothing was sent and nothing was queued, so
3205
+ * that amended retry is safe.
3206
+ */
3207
+ declare const REVIEW_PROBLEM_RETRYABLE: Readonly<Record<string, boolean>>;
3208
+ /** One per-field validation detail in a {@link Problem}. */
3209
+ interface ProblemField {
3210
+ field?: string;
3211
+ code?: string;
3212
+ detail?: string;
3213
+ }
3214
+ /** The RFC-9457 problem+json body. */
3215
+ interface Problem {
3216
+ /** Dereferenceable URI under `https://extrovert.dev/problems/{code}`. */
3217
+ type: string;
3218
+ /** Short, human-readable summary of the problem type. */
3219
+ title: string;
3220
+ /** HTTP status code, duplicated in the body. */
3221
+ status: number;
3222
+ /** Human-readable, instance-specific detail. */
3223
+ detail?: string;
3224
+ /** The closed machine code clients switch on. */
3225
+ code: ProblemCode;
3226
+ /** Per-request id for support correlation. */
3227
+ request_id?: string;
3228
+ /** Optional machine hints (e.g. `breadth_required` may name the next calls). */
3229
+ errors?: ProblemField[];
3230
+ }
3231
+ /** True when `value` is a member of the closed {@link ProblemCode} enum. */
3232
+ declare function isProblemCode(value: unknown): value is ProblemCode;
3233
+ /**
3234
+ * Parse an unknown JSON body into a {@link Problem} when it carries the required
3235
+ * problem+json shape (`code` in the closed enum), else `undefined`. Unknown codes
3236
+ * are coerced to `"internal"` so the typed union holds while the raw value is still
3237
+ * available via {@link ProblemError.rawCode}.
3238
+ */
3239
+ declare function parseProblem(body: unknown): {
3240
+ problem: Problem;
3241
+ rawCode: string;
3242
+ } | undefined;
3243
+
3244
+ /**
3245
+ * Extrovert error types. Every non-2xx response is surfaced as an {@link ApiError} (or a subclass),
3246
+ * so callers can branch on `status` / `code` without inspecting raw responses.
3247
+ *
3248
+ * The redesigned surface returns RFC-9457 `application/problem+json` with a CLOSED
3249
+ * machine {@link ProblemCode} enum. When that body is present, the thrown error
3250
+ * carries the parsed {@link Problem} on `.problem` and a typed `.problemCode`; the
3251
+ * legacy `{ error, message }` envelope is still parsed for back-compat.
3252
+ */
3253
+
3254
+ /** The machine-readable error envelope the Extrovert API returns on failures. */
3255
+ interface ApiErrorBody {
3256
+ error: {
3257
+ /** Stable, machine-readable code, e.g. `enrollment_token_exhausted`, `not_found`. */
3258
+ code: string;
3259
+ /** Human-readable message safe to surface to developers. */
3260
+ message: string;
3261
+ /** Optional per-field validation detail. */
3262
+ details?: Record<string, unknown>;
3263
+ };
3264
+ /** Request id for support correlation, echoed from the `X-Request-Id` header. */
3265
+ request_id?: string;
3266
+ }
3267
+ /**
3268
+ * Base error for any failed Extrovert API call. Carries the HTTP `status`, the stable `code`, the
3269
+ * `request_id` for support, and the raw `body` for forward-compatible inspection.
3270
+ */
3271
+ declare class ApiError extends Error {
3272
+ /** HTTP status code (e.g. 401, 404, 409, 429). 0 when the request never reached the server. */
3273
+ readonly status: number;
3274
+ /** Stable machine-readable error code from the body, or a synthesized one for transport errors. */
3275
+ readonly code: string;
3276
+ /** Request id for support correlation, when present. */
3277
+ readonly requestId: string | undefined;
3278
+ /** The parsed error body, when the server returned one. */
3279
+ readonly body: ApiErrorBody | undefined;
3280
+ /**
3281
+ * The parsed RFC-9457 problem+json body, when the server returned one. Present on
3282
+ * the redesigned surface; `undefined` for the legacy `{ error }` envelope.
3283
+ */
3284
+ readonly problem: Problem | undefined;
3285
+ /**
3286
+ * The typed closed-enum problem code, when the server returned problem+json. An
3287
+ * unknown/legacy code is coerced to `"internal"`; the raw string is always on
3288
+ * {@link ApiError.code}.
3289
+ */
3290
+ readonly problemCode: ProblemCode | undefined;
3291
+ constructor(args: {
3292
+ status: number;
3293
+ code: string;
3294
+ message: string;
3295
+ requestId?: string | undefined;
3296
+ body?: ApiErrorBody | undefined;
3297
+ problem?: Problem | undefined;
3298
+ cause?: unknown;
3299
+ });
3300
+ /** True for 4xx responses (client errors that retrying won't fix). */
3301
+ get isClientError(): boolean;
3302
+ /** True for 5xx responses (server errors that may succeed on retry). */
3303
+ get isServerError(): boolean;
3304
+ }
3305
+ /** 401 — the agent key / enrollment token was missing, malformed, expired, or revoked. */
3306
+ declare class AuthenticationError extends ApiError {
3307
+ }
3308
+ /** 403 — authenticated, but the key's scopes don't permit this action (capability denied). */
3309
+ declare class PermissionError extends ApiError {
3310
+ }
3311
+ /**
3312
+ * 403 `forbidden_scope` — the call is outside the key's CEILING (e.g. a non-org key
3313
+ * on the org-wide wildcard, or a mint that would escalate). A redesign-specific
3314
+ * subclass of {@link PermissionError} so existing `instanceof PermissionError`
3315
+ * branches keep working.
3316
+ */
3317
+ declare class ForbiddenScopeError extends PermissionError {
3318
+ }
3319
+ /**
3320
+ * 400 `breadth_required` — an org-tier key/operator issued a bare list that needs a
3321
+ * breadth pick; the problem `errors`/`detail` name the next call
3322
+ * (`/v1/projects/{id}/inboxes` or `/v1/projects/-/inboxes`).
3323
+ */
3324
+ declare class BreadthRequiredError extends ApiError {
3325
+ }
3326
+ /** 404 — the inbox, message, thread, or webhook does not exist (or isn't visible to this tenant). */
3327
+ declare class NotFoundError extends ApiError {
3328
+ }
3329
+ /** 409 — a conflicting state, e.g. an enrollment token that already minted its max of N inboxes. */
3330
+ declare class ConflictError extends ApiError {
3331
+ }
3332
+ /** 422 — the request body failed validation; see `body.error.details`. */
3333
+ declare class ValidationError extends ApiError {
3334
+ }
3335
+ /**
3336
+ * 422 `recipient_suppressed` — a send/reply/forward was rejected because one or
3337
+ * more recipients have opted out (list-unsubscribe / suppression). The whole send
3338
+ * is rejected (never a silent partial drop). {@link suppressedRecipients} lists the
3339
+ * exact addresses to drop; retry the send without them. The scope/origin of the
3340
+ * opt-out is deliberately NOT surfaced. A subclass of {@link ValidationError} so
3341
+ * existing `instanceof ValidationError` branches keep working.
3342
+ */
3343
+ declare class RecipientSuppressedError extends ValidationError {
3344
+ /** The recipient addresses that are suppressed — drop these and retry. */
3345
+ readonly suppressedRecipients: string[];
3346
+ constructor(args: ConstructorParameters<typeof ApiError>[0]);
3347
+ }
3348
+ /**
3349
+ * 422 `intent_required` — the inbox's resolved review policy requires a human to
3350
+ * see this message before it goes out, and the request carried no `intent`.
3351
+ *
3352
+ * **Nothing was sent and nothing was queued.** The server checks this before it
3353
+ * writes any row, which is what makes the amended retry safe: add an `intent`
3354
+ * and re-POST the SAME request. {@link retryWith} is the literal JSON fragment to
3355
+ * splice in, and the human-readable remediation (the full recipe, including how
3356
+ * to monitor the resulting review) is on `.message` / `.problem.detail`.
3357
+ *
3358
+ * Under `require_review` — the default for every account — this is the FIRST
3359
+ * thing most agents hit. Read `effective_review_policy` on
3360
+ * `GET /v1/inboxes/{id}` once at start-up and compose an intent up front instead
3361
+ * of learning the policy by being refused. A subclass of {@link ValidationError}
3362
+ * so existing `instanceof ValidationError` branches keep working.
3363
+ */
3364
+ declare class IntentRequiredError extends ValidationError {
3365
+ /** The resolved review policy, e.g. `require_review`. */
3366
+ readonly policy: string | undefined;
3367
+ /** Where the policy came from — a per-inbox override or the account default. */
3368
+ readonly policySource: string | undefined;
3369
+ /** Literal JSON to merge into the original request body, then retry once. */
3370
+ readonly retryWith: string | undefined;
3371
+ constructor(args: ConstructorParameters<typeof ApiError>[0]);
3372
+ }
3373
+ /**
3374
+ * Base for the review-loop 409s. Carries the live state and CAS keys the server
3375
+ * attached so a retry (where one is legal at all) needs no extra `get_review`,
3376
+ * and the `allowed_action` verbs that ARE legal from the current state so an
3377
+ * agent is told what to do instead of guessing.
3378
+ *
3379
+ * Extends {@link ConflictError}: every one of these used to be a bare 409, and
3380
+ * nothing that catches `ConflictError` should have to change.
3381
+ */
3382
+ declare class ReviewConflictError extends ConflictError {
3383
+ /** The review's CURRENT state (`needs_review`, `approved`, `sent`, …). */
3384
+ readonly currentState: string | undefined;
3385
+ /** The current revision — pass it as `parent_revision` on a legal retry. */
3386
+ readonly currentRevision: number | undefined;
3387
+ /** The current row version (the optional belt-and-braces CAS). */
3388
+ readonly currentVersion: number | undefined;
3389
+ /** The verbs that ARE legal from {@link currentState}, e.g. `submit_revision`. */
3390
+ readonly allowedActions: string[];
3391
+ constructor(args: ConstructorParameters<typeof ApiError>[0]);
3392
+ /**
3393
+ * Whether retrying the same call could ever succeed. False for every subclass
3394
+ * except {@link StaleError} and {@link BornStaleError} — and true there only
3395
+ * after re-reading and re-applying on top of the other party's change.
3396
+ */
3397
+ get isRetryable(): boolean;
3398
+ }
3399
+ /**
3400
+ * 409 `stale` — the `(revision[, version])` you named is no longer current
3401
+ * because a human or reviewer moved the draft. **Nothing was mutated.**
3402
+ *
3403
+ * The one genuinely retryable conflict, and bounded (≤3): re-read the draft and
3404
+ * the feedback, re-apply your edit on top of theirs, resubmit with
3405
+ * {@link ReviewConflictError.currentRevision} as the new `parent_revision`. The
3406
+ * current state/revision/version ride along on the error, so the retry costs no
3407
+ * extra round trip.
3408
+ */
3409
+ declare class StaleError extends ReviewConflictError {
3410
+ get isRetryable(): boolean;
3411
+ }
3412
+ /**
3413
+ * 409 `wrong_state` — this VERB is illegal from the review's current state, but
3414
+ * the draft is still live.
3415
+ *
3416
+ * **Never retry the same verb**; the timing is not the problem, the choice of
3417
+ * call is. Read {@link ReviewConflictError.allowedActions} and pick one of those.
3418
+ */
3419
+ declare class WrongStateError extends ReviewConflictError {
3420
+ }
3421
+ /**
3422
+ * 409 `terminal` — the review has already finished (sent / auto_sent /
3423
+ * cancelled). Nothing will EVER succeed on it.
3424
+ *
3425
+ * **Stop.** A `front_run_next` review event is waiting on the durable queue with
3426
+ * the outcome; drain it, ack it, and compose a NEW message if one is still
3427
+ * wanted. {@link sentMessageId} is the message that actually went out, when the
3428
+ * review reached a delivery.
3429
+ */
3430
+ declare class TerminalError extends ReviewConflictError {
3431
+ /** The message that was actually delivered, when this review sent one. */
3432
+ readonly sentMessageId: string | undefined;
3433
+ constructor(args: ConstructorParameters<typeof ApiError>[0]);
3434
+ }
3435
+ /**
3436
+ * 409 `born_stale` — the redraft was composed against an OLDER writing-rule
3437
+ * high-water than the one now in force. **Nothing was mutated** and the composer
3438
+ * has been re-nudged.
3439
+ *
3440
+ * Retryable at most once per rule high-water: re-read the rules, re-apply them,
3441
+ * resubmit — or `restamp_review` when re-reading shows nothing genuinely needed
3442
+ * to change. Restamping when the body DID need to change makes the draft lie to
3443
+ * the born-stale accounting, so do it only for a true no-op.
3444
+ */
3445
+ declare class BornStaleError extends ReviewConflictError {
3446
+ get isRetryable(): boolean;
3447
+ }
3448
+ /**
3449
+ * 409 `send_needs_reconciliation` — a delivery attempt reached (or may have
3450
+ * reached) the mail provider and the process died before recording the outcome,
3451
+ * so the review is parked for recover-by-Message-ID.
3452
+ *
3453
+ * **Do not resend.** That is exactly how one message goes out twice. Poll the
3454
+ * review (`closed` / `sent_message_id`) until an operator or the recovery path
3455
+ * resolves it.
3456
+ */
3457
+ declare class SendNeedsReconciliationError extends ReviewConflictError {
3458
+ }
3459
+ /**
3460
+ * 409 `idempotency_conflict` — the same `Idempotency-Key` was replayed with a
3461
+ * DIFFERENT request body within the same scope. The replay key is a hash of the
3462
+ * raw bytes, so "same message, different spelling" counts as different.
3463
+ *
3464
+ * A caller bug, not a race: do not retry under that key. Either send the byte-
3465
+ * identical body, or mint a new key for the genuinely new message.
3466
+ */
3467
+ declare class IdempotencyConflictError extends ConflictError {
3468
+ }
3469
+ /**
3470
+ * 503 `unavailable` — a dependency could not be read, so the request was failed
3471
+ * CLOSED rather than served on a guess. On the send path this specifically means
3472
+ * the account's review policy was unreadable: relaying unsupervised mail for a
3473
+ * customer whose stated policy we could not see is the failure that would be
3474
+ * worse than the outage.
3475
+ *
3476
+ * Retryable after {@link retryAfter} seconds. Distinct from `not_configured`,
3477
+ * which means the deployment does not have the capability at all.
3478
+ */
3479
+ declare class UnavailableError extends ApiError {
3480
+ /** Seconds to wait before retrying, from the `Retry-After` header. */
3481
+ readonly retryAfter: number | undefined;
3482
+ constructor(args: ConstructorParameters<typeof ApiError>[0] & {
3483
+ retryAfter?: number;
3484
+ });
3485
+ }
3486
+ /** 402 — payment required (x402 test-mode). `paymentRequired` holds the raw challenge header. */
3487
+ declare class PaymentRequiredError extends ApiError {
3488
+ /** The raw `PAYMENT-REQUIRED` header challenge to sign + retry (EIP-3009, Base Sepolia). */
3489
+ readonly paymentRequired: string | undefined;
3490
+ constructor(args: ConstructorParameters<typeof ApiError>[0] & {
3491
+ paymentRequired?: string;
3492
+ });
3493
+ }
3494
+ /** 429 — rate limited. `retryAfter` is the server's hint in seconds, when provided. */
3495
+ declare class RateLimitError extends ApiError {
3496
+ /** Seconds to wait before retrying, parsed from the `Retry-After` header. */
3497
+ readonly retryAfter: number | undefined;
3498
+ constructor(args: ConstructorParameters<typeof ApiError>[0] & {
3499
+ retryAfter?: number;
3500
+ });
3501
+ }
3502
+ /** The request failed before a response was received (network down, DNS, abort, timeout). */
3503
+ declare class ConnectionError extends ApiError {
3504
+ constructor(message: string, cause?: unknown);
3505
+ }
3506
+ /** The request was aborted by the caller's `AbortSignal` or the client `timeout`. */
3507
+ declare class TimeoutError extends ApiError {
3508
+ constructor(message?: string, cause?: unknown);
3509
+ }
3510
+
3511
+ /**
3512
+ * Narrowing helpers for {@link SendOutcome} — the three shapes a send can answer.
3513
+ *
3514
+ * `inbox.send()` used to be typed as one struct with a REQUIRED `thread_id`, which
3515
+ * the direct-send response has never carried. The type checked; the value was
3516
+ * `undefined`. Now the return type is an honest union, and these helpers exist so
3517
+ * that reading it does not turn into a `"kind" in res` puzzle at every call site.
3518
+ *
3519
+ * The one rule worth internalizing: a `queued_for_review` outcome means **nothing
3520
+ * has been delivered**. A human has to approve it first, and the delivery outcome
3521
+ * arrives later as a `sent` / `send_failed` review event. Code that treats every
3522
+ * 2xx from `send()` as "the mail went out" is wrong under the default
3523
+ * `require_review` policy — which is every account that has not changed it.
3524
+ */
3525
+
3526
+ /**
3527
+ * True when the message was PARKED for a human and nothing has been delivered.
3528
+ * Narrow with this before reading `res.review.id`, then monitor the review via
3529
+ * `extrovert.reviewEvents.wait({ review_id })`.
3530
+ */
3531
+ declare function isQueuedForReview(res: SendOutcome): res is QueuedForReviewResult;
3532
+ /**
3533
+ * True when the message was delivered immediately — either the review-loop
3534
+ * `{kind:"sent"}` body or the legacy body a bare send gets under `allow_direct`.
3535
+ */
3536
+ declare function isSentImmediately(res: SendOutcome): res is SendResult | SentResult;
3537
+ /**
3538
+ * The delivered message id, or `undefined` when the message was queued instead.
3539
+ *
3540
+ * `undefined` here is NOT an error — it is the normal answer under
3541
+ * `require_review`. Pair it with {@link reviewIdOf} to follow the message to its
3542
+ * outcome.
3543
+ */
3544
+ declare function sentMessageIdOf(res: SendOutcome): string | undefined;
3545
+ /**
3546
+ * The thread id when one is known.
3547
+ *
3548
+ * Absent on a direct `send` (the server assigns the thread at delivery and the
3549
+ * legacy body does not echo it) and on a queued outcome (there is no message
3550
+ * yet). Reply and forward do return it.
3551
+ */
3552
+ declare function threadIdOf(res: SendOutcome): string | undefined;
3553
+ /**
3554
+ * The opaque review id (rr_…) that governed this send, on EVERY branch.
3555
+ *
3556
+ * This is the crash-recovery handle: an agent that issued a send and then died
3557
+ * can call `reviews.get(id)` and read `closed` / `send_error` / `sent_message_id`
3558
+ * rather than guessing whether the message went out. It is `undefined` only
3559
+ * against a server old enough to predate the field.
3560
+ */
3561
+ declare function reviewIdOf(res: SendOutcome): string | undefined;
3562
+
3563
+ /**
3564
+ * Dated API version pin (redesign §5.4).
3565
+ *
3566
+ * The Extrovert API is versioned by a dated `Extrovert-Version` request header. A
3567
+ * request that omits the header is served the latest version; a pinned older
3568
+ * version is transformed to the current shape by a server-side shim. The SDK pins
3569
+ * {@link CURRENT_API_VERSION} on every request by default so an app's behavior is
3570
+ * stable across server deploys; override it per client via
3571
+ * `new Extrovert({ apiVersion: "YYYY-MM-DD" })`.
3572
+ */
3573
+ /** The dated API version this SDK was built against. Sent as `Extrovert-Version`. */
3574
+ declare const CURRENT_API_VERSION = "2026-06-23";
3575
+ /** The HTTP header carrying the dated API version (request + echoed on the response). */
3576
+ declare const API_VERSION_HEADER = "Extrovert-Version";
3577
+
3578
+ /**
3579
+ * OTP / verification-link extraction.
3580
+ *
3581
+ * Find the most likely one-time code and preferred verification link in a message body. Pure and
3582
+ * dependency-free so it runs in Node, the edge, and the browser.
3583
+ */
3584
+
3585
+ /** Extract the most likely OTP code from a body of text, or null. */
3586
+ declare function extractOtp(body: string): string | null;
3587
+ /** Extract the most likely verification link from a body, or null. */
3588
+ declare function extractLink(body: string): string | null;
3589
+ /**
3590
+ * Extract OTP + verification link from a message. Prefers the plain-text body; falls back to a
3591
+ * tag-stripped HTML body so codes/links rendered only in HTML are still found.
3592
+ */
3593
+ declare function extractCredentials(message: Message): ExtractedCredentials;
3594
+
3595
+ /**
3596
+ * Inbound webhook verification.
3597
+ *
3598
+ * Extrovert signs every inbound delivery with an HMAC-SHA256 over `${timestamp}.${rawBody}` and sends
3599
+ * `X-Extrovert-Signature: t=<unix>,v1=<hex>` (§6, §14). Verify with the per-webhook `secret` returned
3600
+ * once at registration. Uses Web Crypto (`crypto.subtle`), so it works in Node 18+, Cloudflare
3601
+ * Workers, Vercel Edge, Deno, and the browser without any dependency.
3602
+ */
3603
+
3604
+ /** The decoded body of a `message.received` (and similar) webhook delivery. */
3605
+ interface WebhookPayload {
3606
+ event: WebhookEvent;
3607
+ /** Delivery id, unique per attempt. */
3608
+ id: string;
3609
+ created_at: string;
3610
+ /** The message that triggered the event (present for message.* events). */
3611
+ message?: Message;
3612
+ /** The inbox address the event concerns. */
3613
+ inbox?: string;
3614
+ }
3615
+ interface VerifyWebhookOptions {
3616
+ /** Raw request body, exactly as received (do not re-serialize JSON before verifying). */
3617
+ payload: string;
3618
+ /** The `X-Extrovert-Signature` header value. */
3619
+ signature: string;
3620
+ /** The webhook signing secret (`whsec_...`). */
3621
+ secret: string;
3622
+ /** Reject deliveries whose timestamp is older than this many seconds. Default 300 (5 min). */
3623
+ toleranceSeconds?: number;
3624
+ /** Override the clock (unix seconds) for deterministic testing. */
3625
+ nowSeconds?: number;
3626
+ }
3627
+ /**
3628
+ * Verify an inbound webhook signature. Returns true when the signature is valid and within the
3629
+ * timestamp tolerance. Throws only if the runtime lacks Web Crypto.
3630
+ *
3631
+ * @example
3632
+ * ```ts
3633
+ * const ok = await verifyWebhookSignature({
3634
+ * payload: rawBody,
3635
+ * signature: req.headers["x-extrovert-signature"],
3636
+ * secret: process.env.EXTROVERT_WEBHOOK_SECRET!,
3637
+ * });
3638
+ * if (!ok) return new Response("bad signature", { status: 400 });
3639
+ * ```
3640
+ */
3641
+ declare function verifyWebhookSignature(options: VerifyWebhookOptions): Promise<boolean>;
3642
+ /**
3643
+ * Verify a webhook and parse its JSON body in one step. Returns the typed payload on success, or
3644
+ * null when the signature is invalid (so callers can branch without a try/catch on the common path).
3645
+ */
3646
+ declare function parseWebhook(options: VerifyWebhookOptions): Promise<WebhookPayload | null>;
3647
+ /**
3648
+ * Produce the canonical `X-Extrovert-Signature` header value for a body — the exact format the Go
3649
+ * delivery engine emits: `t=<unix>,v1=<hex hmac-sha256("<t>.<rawbody>")>`. Mainly useful for tests
3650
+ * and self-hosted senders; the platform signs deliveries server-side. The Go `SignWebhook` and this
3651
+ * helper are pinned to the same fixed conformance vector across languages.
3652
+ */
3653
+ declare function signWebhook(secret: string, body: string, timestampSeconds: number): Promise<string>;
3654
+
3655
+ /**
3656
+ * The Extrovert Review-Loop **open contract** (HITL D14, spec §11).
3657
+ *
3658
+ * This module is the single, documented, *versioned* publication of the stable
3659
+ * agent-facing JSON shapes that the Review Loop exposes — the shapes agents and
3660
+ * third-party harnesses code against. It does **not** redesign any types: it
3661
+ * re-exports the canonical models built across M1–M8 (see `./models`) under one
3662
+ * named contract surface, stamps a {@link CONTRACT_VERSION}, and publishes a
3663
+ * machine-readable {@link CONTRACT_MANIFEST} so a harness can pin the exact set of
3664
+ * shapes + the version it built against.
3665
+ *
3666
+ * ## This is a contract, NOT a protocol (D14)
3667
+ *
3668
+ * Per resolved decision **D14**, the open surface is published **now** as an open,
3669
+ * documented **skill + SDK contract** — explicitly **not** a wire protocol and
3670
+ * **not** a standalone `/v1/contract` endpoint. The contract is exactly: these SDK
3671
+ * types + the agent skills (`extrovert-send-email`, `extrovert-writing-rules`) + the
3672
+ * docs, **versioned with the SDK** (this package). Formal protocol
3673
+ * standardization is deferred (spec §12).
3674
+ *
3675
+ * ## Provisional, pre-1.0 (0.x)
3676
+ *
3677
+ * {@link CONTRACT_VERSION} is **`0.1.0-pre.3`** — a deliberately **provisional**, pre-1.0
3678
+ * contract. It is open and documented, but it MAY still evolve before 1.0: there
3679
+ * are no external users yet, and the **D20 shared-pool auto-send governor** is a
3680
+ * hard prerequisite before onboarding external users. Pin the version; expect
3681
+ * additive 0.x changes. See {@link CONTRACT_MANIFEST}.`stability`.
3682
+ *
3683
+ * ## Identity (D10)
3684
+ *
3685
+ * Every shape keys on **opaque, typed ids** (`rr_`, `turn_`, `cat_`, `rule_`,
3686
+ * `rln_`, `ndg_`, …) and never on names. Names/descriptions are mutable display
3687
+ * metadata; renaming never breaks a reference. $0-LLM on our side — the contract
3688
+ * is pure deterministic JSON; all judgment lives in the agent skills.
3689
+ *
3690
+ * The canonical example payloads for the §11 core shapes (Intent, ReviewFeedback,
3691
+ * DiffJson, Rule, Nudge) are the conformance golden fixtures — see
3692
+ * `golang/internal/extrovertapi/testdata/contract/` and the SDK
3693
+ * `contract.test.ts` (both assert these examples parse/validate without loss).
3694
+ *
3695
+ * @packageDocumentation
3696
+ */
3697
+ /** §11 "Intent (submit time)". The agent's for-the-reviewer summary (D3). */
3698
+
3699
+ /**
3700
+ * One structured hunk of a `diff_json` (spec §11 "Diff (`diff_json`)"). A single
3701
+ * deterministic field change computed server-side in Go ($0 LLM). `op` is the
3702
+ * change verb; `before`/`after` carry the literal text.
3703
+ */
3704
+ interface DiffHunk {
3705
+ /** The Review field this hunk changed (e.g. `subject`, `body_text`). */
3706
+ field: string;
3707
+ /** The change verb (e.g. `replace`, `insert`, `delete`). */
3708
+ op: string;
3709
+ /** The text before the change. */
3710
+ before?: string;
3711
+ /** The text after the change. */
3712
+ after?: string;
3713
+ }
3714
+ /**
3715
+ * §11 "Diff (`diff_json`)". The structured proposed-vs-sent diff, computed
3716
+ * deterministically in Go ($0 LLM). `fields_changed` names the changed Review
3717
+ * fields; `hunks` carry the per-field before/after.
3718
+ *
3719
+ * This is the named publication of the `diff_json` object that already appears
3720
+ * (as `diff_json`) on `ReviewFeedback` and `ReviewTurn`; those fields keep their
3721
+ * permissive `Record<string, unknown>` wire type for back-compat, and a harness
3722
+ * MAY narrow them to this shape.
3723
+ */
3724
+ interface DiffJson {
3725
+ /** The Review fields that changed (e.g. `["subject","body_text"]`). */
3726
+ fields_changed: string[];
3727
+ /** Per-field structured before/after hunks. */
3728
+ hunks: DiffHunk[];
3729
+ }
3730
+
3731
+ /**
3732
+ * The published version of the Extrovert Review-Loop open contract (D14).
3733
+ *
3734
+ * **`0.1.0-pre.3` — PROVISIONAL, pre-1.0.** Versioned *with the SDK* (this package's
3735
+ * `package.json` version) and aligned to the openapi `info.version`. Open and
3736
+ * documented, but MAY still evolve before 1.0 (no external users yet; the D20
3737
+ * shared-pool governor is required before external users). Pin it.
3738
+ */
3739
+ declare const CONTRACT_VERSION: "0.1.0-pre.3";
3740
+ /** The stability posture of a published contract version. */
3741
+ type ContractStability = "provisional" | "stable";
3742
+ /**
3743
+ * The machine-readable manifest of the open contract (D14) — what a harness pins.
3744
+ *
3745
+ * It enumerates the canonical §11 **core** shapes and the **full** M1–M8 surface
3746
+ * by name, stamps {@link CONTRACT_VERSION}, and marks the {@link ContractStability}
3747
+ * posture so a consumer can reason about evolution risk. It carries no runtime
3748
+ * behavior (M9 adds none — types + a version + a test + docs) and no LLM.
3749
+ */
3750
+ interface ContractManifest {
3751
+ /** Stable contract name (NOT a protocol name — D14). */
3752
+ readonly name: "extrovert.review-loop";
3753
+ /** The published contract version (== {@link CONTRACT_VERSION}). */
3754
+ readonly version: string;
3755
+ /**
3756
+ * `provisional` (0.x) until the D20 shared-pool governor lands + external users
3757
+ * exist. Provisional ⇒ additive evolution before 1.0 is expected.
3758
+ */
3759
+ readonly stability: ContractStability;
3760
+ /**
3761
+ * D14: this is an SDK + skill contract, versioned WITH the SDK — never a wire
3762
+ * protocol or a standalone protocol endpoint.
3763
+ */
3764
+ readonly kind: "sdk+skill-contract";
3765
+ /** Cross-reference to the spec section that froze these shapes. */
3766
+ readonly spec_ref: "hitl-spec.md#11";
3767
+ /** The five canonical §11 shapes, named verbatim from the spec. */
3768
+ readonly core_shapes: readonly string[];
3769
+ /** The full published M1–M8 agent-facing contract surface (one 0.x contract; no tiering). */
3770
+ readonly shapes: readonly string[];
3771
+ /** The agent skills that are part of the contract (D14 — "skill + SDK"). */
3772
+ readonly skills: readonly string[];
3773
+ }
3774
+ /**
3775
+ * The published manifest instance. Frozen so a harness can compare it
3776
+ * structurally. The `core_shapes` are the five §11 canonical shapes; `shapes` is
3777
+ * the full provisional-0.x surface. Keep this list in sync with the re-exports
3778
+ * above — the `contract.test.ts` drift test asserts every named shape resolves.
3779
+ */
3780
+ declare const CONTRACT_MANIFEST: ContractManifest;
3781
+
3782
+ export { API_VERSION_HEADER, type AccountRiskDial, type AckReviewEventEntry, type AckReviewEventRequest, type AckReviewEventResult, type AddContactListRequest, type Agent, type AgentStatus, ApiError, type ApiErrorBody, type Attachment, type AttachmentDownload, type AttachmentInput, AuthenticationError, type BatchUpdateMessagesRequest, type BatchUpdateResult, BornStaleError, BreadthRequiredError, CONTRACT_MANIFEST, CONTRACT_VERSION, CURRENT_API_VERSION, Categories, type Category, type CategoryPacingState, type CategoryRiskDial, ConflictError, ConnectionError, type ContactListDirection, type ContactListEntry, type ContactListKind, ContactLists, type ContractManifest, type ContractStability, type CreateInboxRequest, type Cursor, DEFAULT_BASE_URL, type DeleteResult, type DiffHunk, type DiffJson, type Domain, type DomainOffboard, type DomainRecord, type DomainScope, Domains, type EffectiveRiskDial, type EmailAddress, type EnrollRequest, type EnrollResponse, type ExtractedCredentials, ExtrovertClient as Extrovert, ExtrovertClient, type ExtrovertClientOptions, ForbiddenScopeError, type ForwardRequest, type GetInboxParams, type GetRuleAuditParams, type GetRulesParams, type GraduationStatus, IdempotencyConflictError, type Inbox, type InboxCredentials, InboxHandle, type InboxInclude, type InboxMetadata, type InboxMetadataPatch, type InboxMetadataValue, type InboxStatus, Inboxes, IntentRequiredError, type IsoTimestamp, type Job, type KeyTier, type List, type ListCategoriesParams, type ListInboxesParams, type ListMessagesParams, ListPage, type ListParams, type ListReviewEventsParams, type ListReviewsParams, type ListSuppressionsParams, type ListThreadsParams, MOCK_BASE_URL, type MailFolder, type MarkReadRequest, type Message, type MessageDirection, Messages, MockBackend, NotFoundError, type OnboardDomainRequest, type OnboardingMode, PROBLEM_CODES, type PacingItem, type Page, type PageFetcher, PaymentRequiredError, PermissionError, type PostReviewChatRequest, type Problem, type ProblemCode, type ProblemField, type ProjectInboxListParams, ProjectInboxes, Projects, type ProposeCategoryRequest, type ProposeGraduationRequest, type QueuedForReviewResult, REVIEW_PROBLEM_RETRYABLE, RateLimitError, RecipientSuppressedError, type RegisterWebhookRequest, type ReplyRequest, type RestampReviewRequest, type Review, ReviewConflictError, type ReviewDecisionContext, type ReviewEvent, type ReviewEventCursor, type ReviewEventReason, ReviewEvents, type ReviewEventsResult, type ReviewFeedback, type ReviewFeedbackComment, type ReviewInclude, type ReviewIntent, type ReviewMode, type ReviewPolicy, type ReviewState, type ReviewTurn, type ReviewerAction, type ReviewerDecisionRequest, type ReviewerDecisionResult, Reviews, type RiskDial, type Rule, type RuleAuditEntry, type RuleLayer, Rules, SDK_VERSION, type SaveRuleRequest, type ScanBacklogStatus, type Scope, type SearchMessagesParams, SendNeedsReconciliationError, type SendOutcome, type SendRequest, type SendResult, type SentResult, type SignUpRequest, type SignUpResponse, StaleError, type StreamEvent, type StreamOptions, type SubmitForReviewResult, type SubmitRevisionRequest, type SuppressionEntry, type SuppressionPrecheck, type SuppressionScope, type SuppressionSource, Suppressions, TerminalError, type Thread, type ThreadDetail, Threads, TimeoutError, UnavailableError, type UpdateCategoryRequest, type UpdateInboxRequest, type UpdateWebhookRequest, ValidationError, type VerifyRequest, type VerifyResponse, type VerifyWebhookOptions, type WaitForEmailRequest, type WaitForEmailResult, type WaitForReviewEventParams, type Webhook, type WebhookEvent, type WebhookPayload, Webhooks, type WhoAmI, WrongStateError, extractCredentials, extractLink, extractOtp, isProblemCode, isQueuedForReview, isSentImmediately, listPage, parseKeyTier, parseProblem, parseWebhook, reviewIdOf, sentMessageIdOf, serializeInclude, signWebhook, threadIdOf, tierAllowsOrgWildcard, tierNeedsExplicitBreadth, verifyWebhookSignature };