@extrovert.dev/sdk 0.1.0-pre.5 → 0.1.0-pre.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -65
- package/dist/index.cjs +514 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +544 -280
- package/dist/index.d.ts +544 -280
- package/dist/index.js +514 -118
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
**A real inbox for your agent, in one call.**
|
|
6
6
|
|
|
7
|
-
The TypeScript SDK for [Extrovert](https://extrovert.dev)
|
|
7
|
+
The TypeScript SDK for [Extrovert](https://extrovert.dev): Message Science's agent-email
|
|
8
8
|
platform. Extrovert gives an AI agent a real, persistent inbox on a domain we own: created in one
|
|
9
9
|
call, sends and receives, behind a scoped key that expires and revokes on its own.
|
|
10
10
|
|
|
11
|
-
- **One call to a live inbox.** `agent7@
|
|
12
|
-
|
|
11
|
+
- **One call to a live inbox.** Paid accounts use `agent7@extrovertmail.com`; free
|
|
12
|
+
signups use `agent7@free.extrovertmail.com`. No DNS setup is required.
|
|
13
13
|
- **`waitForEmail`, the killer primitive.** Block until the next matching message lands and get the
|
|
14
14
|
OTP code / verification link extracted as a structured field. No polling loop.
|
|
15
15
|
- **Typed everything.** Request and response models matching the Extrovert `/v1` contract, a typed
|
|
16
16
|
`ApiError` hierarchy, full `.d.ts` declarations.
|
|
17
17
|
- **Runs where your agent runs.** Pure `fetch`. Node 18+, Cloudflare Workers, Vercel Edge, Deno,
|
|
18
18
|
the browser. Zero runtime dependencies.
|
|
19
|
-
- **Don't hand an MCP host your master key.** Redeem a scoped enrollment key;
|
|
19
|
+
- **Don't hand an MCP host your master key.** Redeem a scoped enrollment key; issue an agent key that
|
|
20
20
|
expires and revokes on its own.
|
|
21
21
|
|
|
22
22
|
---
|
|
@@ -30,7 +30,7 @@ for a stable release:
|
|
|
30
30
|
npm install @extrovert.dev/sdk@next
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Pin `@extrovert.dev/sdk@0.1.0-pre.
|
|
33
|
+
Pin `@extrovert.dev/sdk@0.1.0-pre.7` when a dogfood test needs a reproducible contract snapshot.
|
|
34
34
|
Requires Node 18+ for global `fetch` and Web Crypto.
|
|
35
35
|
|
|
36
36
|
## Build and use from source
|
|
@@ -48,7 +48,11 @@ REST API and served OpenAPI document remain the underlying contract.
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
## Quickstart
|
|
51
|
+
## Quickstart: inbox in one call
|
|
52
|
+
|
|
53
|
+
Use the scoped key issued for this agent on your existing account. Do not sign up for another
|
|
54
|
+
account to connect a new agent. Confirm the connection with `extrovert.whoami()` before creating
|
|
55
|
+
resources; its capability summary explains what the connection can do.
|
|
52
56
|
|
|
53
57
|
```ts
|
|
54
58
|
import { Extrovert, isQueuedForReview } from "@extrovert.dev/sdk";
|
|
@@ -57,10 +61,10 @@ const extrovert = new Extrovert({ apiKey: process.env.EXTROVERT_API_KEY! });
|
|
|
57
61
|
|
|
58
62
|
// One call. A real, send-and-receive-capable inbox.
|
|
59
63
|
const inbox = await extrovert.inboxes.create({ display_name: "Support Bot" });
|
|
60
|
-
console.log(inbox.address); // agent7@
|
|
64
|
+
console.log(inbox.address); // agent7@extrovertmail.com
|
|
61
65
|
|
|
62
66
|
// Queue a message for a human. `intent.summary` is what the reviewer reads
|
|
63
|
-
// first, and it is REQUIRED
|
|
67
|
+
// first, and it is REQUIRED: under the default `require_review` policy a send
|
|
64
68
|
// without one is refused 422 `intent_required` (nothing sent, nothing queued).
|
|
65
69
|
const outcome = await inbox.send({
|
|
66
70
|
to: "ops@acme.test",
|
|
@@ -77,17 +81,56 @@ if (isQueuedForReview(outcome)) {
|
|
|
77
81
|
|
|
78
82
|
**Your agent queues mail; it does not send it.** A message parks as a review request, a human
|
|
79
83
|
approves / edits / rejects it, and your agent watches for that outcome and redrafts as needed.
|
|
80
|
-
`allow_direct` inboxes and graduated categories are the exceptions, not the default
|
|
84
|
+
`allow_direct` inboxes and graduated categories are the exceptions, not the default: read
|
|
81
85
|
`inbox.record?.effective_review_policy` once rather than learning the policy by being refused. The
|
|
82
86
|
other half of the loop is the [agent contract](https://docs.extrovert.dev/review-loop/agent-contract/).
|
|
83
87
|
|
|
84
|
-
`inboxes.create()` returns an `InboxHandle
|
|
88
|
+
`inboxes.create()` returns an `InboxHandle`: an ergonomic handle bound to one address, so the rest
|
|
85
89
|
of your agent code reads naturally: `inbox.send(...)`, `inbox.messages()`, `inbox.waitForEmail(...)`,
|
|
86
90
|
`inbox.delete()`. This is the curl-style sugar that resolves to your key's default project.
|
|
87
91
|
|
|
88
92
|
---
|
|
89
93
|
|
|
90
|
-
##
|
|
94
|
+
## Check whether your domain is ready
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const domain = await extrovert.domains.get("mail.example.com");
|
|
98
|
+
const readiness = domain.readiness;
|
|
99
|
+
if (!readiness) throw new Error("This server did not return domain readiness.");
|
|
100
|
+
console.log(readiness.label, readiness.summary);
|
|
101
|
+
console.log(readiness.inboxes); // counts visible to this connection, not a global total
|
|
102
|
+
|
|
103
|
+
if (readiness.ready_for_inboxes && readiness.next_action === "create_inbox") {
|
|
104
|
+
console.log("Ready to create an inbox when you want one.");
|
|
105
|
+
} else if (readiness.action_required_by === "extrovert") {
|
|
106
|
+
console.log("Extrovert is handling the next steps. No DNS changes are needed.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A deliberate recheck verifies the DNS entries now; ordinary reads do not.
|
|
110
|
+
// Use after publishing/fixing the entries, not in a tight polling loop.
|
|
111
|
+
await extrovert.domains.verify("mail.example.com");
|
|
112
|
+
|
|
113
|
+
// Wait at most 45 seconds. A timeout does not stop background setup.
|
|
114
|
+
const waiting = await extrovert.domains.wait("mail.example.com", { timeout_seconds: 45 });
|
|
115
|
+
if (waiting.outcome === "timed_out") {
|
|
116
|
+
console.log(`Resume checking in ${waiting.resume_after_seconds} seconds.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Persist next_cursor privately and pass it as after on the next check.
|
|
120
|
+
const updates = await extrovert.domains.events("mail.example.com", { after: "0", limit: 50 });
|
|
121
|
+
for (const event of updates.items) console.log(event.summary);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`waiting_for_dns` means your DNS entries are not confirmed yet; `setting_up` means Extrovert is
|
|
125
|
+
finishing setup. `ready` means mail setup is complete. `action_required` identifies a customer DNS
|
|
126
|
+
repair; `needs_attention` identifies work for Extrovert. Temporary `checking` results are inconclusive,
|
|
127
|
+
not a request to change DNS. Use the returned summary and next action when answering a person.
|
|
128
|
+
|
|
129
|
+
Do not infer readiness from legacy verification or signing fields. Ready inboxes still follow their
|
|
130
|
+
permissions, account limits, and review rules. Setup continues while your agent is disconnected, but
|
|
131
|
+
the agent must stay connected or resume its event/status checks to receive updates.
|
|
132
|
+
|
|
133
|
+
## The canonical chain: `projects.inboxes.*`
|
|
91
134
|
|
|
92
135
|
Scope lives in your **key**, not in headers. A broad (org-tier) key narrows to one **project** by
|
|
93
136
|
path; a project/inbox key is already pinned. The canonical, contract-aligned surface is the
|
|
@@ -98,13 +141,13 @@ within-project alias):
|
|
|
98
141
|
const x = new Extrovert({ apiKey: process.env.EXTROVERT_API_KEY! });
|
|
99
142
|
const { project_id } = await x.whoami(); // the key's fixed project
|
|
100
143
|
|
|
101
|
-
// Create / send / list in a project
|
|
144
|
+
// Create / send / list in a project: keyed by the opaque inbox_id.
|
|
102
145
|
const inbox = await x.projects.inboxes.create(project_id!, { username: "ada" });
|
|
103
146
|
await x.projects.inboxes.send(project_id!, inbox.id, { to: "ops@acme.test", subject: "hi", text: "…",
|
|
104
147
|
intent: { summary: "…one sentence for the human reviewer…" } }); // queues for review
|
|
105
148
|
|
|
106
149
|
// One list envelope: { object: "list", data, has_more, next_cursor }. The ListPage
|
|
107
|
-
// auto-paginates over OPAQUE cursors
|
|
150
|
+
// auto-paginates over OPAQUE cursors: never thread a cursor by hand.
|
|
108
151
|
const page = await x.projects.inboxes.list(project_id!, { limit: 50 });
|
|
109
152
|
for await (const ib of page) console.log(ib.id); // walks every page
|
|
110
153
|
const all = await (await x.projects.inboxes.list(project_id!)).collect(); // eager
|
|
@@ -125,7 +168,7 @@ to opt into the server's transform shim.
|
|
|
125
168
|
|
|
126
169
|
---
|
|
127
170
|
|
|
128
|
-
## The OTP flow
|
|
171
|
+
## The OTP flow: `waitForEmail`
|
|
129
172
|
|
|
130
173
|
Agents sign up for things. The high-value, time-boxed task is "wait for the verification email and
|
|
131
174
|
read the code." Extrovert holds the request open, polls the mailbox server-side, and hands you the extracted code.
|
|
@@ -165,7 +208,7 @@ of real warmup mail for years. Need it standalone? Import `extractOtp` / `extrac
|
|
|
165
208
|
|
|
166
209
|
## Try it offline (no API key, no network)
|
|
167
210
|
|
|
168
|
-
Run the whole SDK against built-in, deterministic fixtures
|
|
211
|
+
Run the whole SDK against built-in, deterministic fixtures: no key, no network. Every method
|
|
169
212
|
works, including a synthesized `waitForEmail` OTP, and the mock models the real review policy,
|
|
170
213
|
so a send without an `intent` is refused offline exactly as it would be live:
|
|
171
214
|
|
|
@@ -173,21 +216,25 @@ so a send without an `intent` is refused offline exactly as it would be live:
|
|
|
173
216
|
const extrovert = new Extrovert({ transport: "mock" });
|
|
174
217
|
const inbox = await extrovert.inboxes.create();
|
|
175
218
|
const { extracted } = await inbox.waitForEmail();
|
|
176
|
-
console.log(extracted.otp); // a fixture OTP
|
|
219
|
+
console.log(extracted.otp); // a fixture OTP: no network touched
|
|
177
220
|
```
|
|
178
221
|
|
|
179
222
|
Set `EXTROVERT_API_BASE_URL=mock` to flip every client into offline mode from the environment.
|
|
180
223
|
|
|
224
|
+
Offline domain setup remains `waiting_for_dns`: it does not query DNS, run background setup, or
|
|
225
|
+
produce lifecycle events. Recheck does not fabricate a ready result. Use a test HTTP response for
|
|
226
|
+
readiness transitions and resumable event scenarios; only the live service confirms actual setup.
|
|
227
|
+
|
|
181
228
|
---
|
|
182
229
|
|
|
183
|
-
## Scoped keys
|
|
230
|
+
## Scoped keys: redeem an enrollment key
|
|
184
231
|
|
|
185
|
-
The identity model: a human (or org-admin call) **
|
|
232
|
+
The identity model: a human (or org-admin call) **issues** a scoped `pk_enroll_...` enrollment key
|
|
186
233
|
that can create up to *N* inboxes and nothing else. An agent **redeems** it for a short-lived,
|
|
187
234
|
individually-revocable `pk_agent_...` key.
|
|
188
235
|
|
|
189
236
|
```ts
|
|
190
|
-
// The agent is handed only the enrollment key
|
|
237
|
+
// The agent is handed only the enrollment key: never an org-wide key.
|
|
191
238
|
const bootstrap = new Extrovert({ apiKey: process.env.EXTROVERT_ENROLLMENT_KEY! });
|
|
192
239
|
|
|
193
240
|
const { client, enrollment } = await bootstrap.enrolled({
|
|
@@ -200,21 +247,21 @@ const { client, enrollment } = await bootstrap.enrolled({
|
|
|
200
247
|
console.log(enrollment.agent_id, enrollment.scopes);
|
|
201
248
|
console.log(enrollment.org_id, enrollment.project_id); // the key's fixed org + project
|
|
202
249
|
|
|
203
|
-
// `client` is already authenticated with the
|
|
250
|
+
// `client` is already authenticated with the issued agent key.
|
|
204
251
|
const inbox = await client.inboxes.create();
|
|
205
252
|
|
|
206
|
-
// The
|
|
253
|
+
// The issued key is bound to a fixed org + project: visible via whoami, never selectable.
|
|
207
254
|
const me = await client.whoami();
|
|
208
255
|
console.log(me.org_id, me.project_id, me.scopes);
|
|
209
256
|
```
|
|
210
257
|
|
|
211
258
|
---
|
|
212
259
|
|
|
213
|
-
## Inbox metadata
|
|
260
|
+
## Inbox metadata: attach your own key-value data
|
|
214
261
|
|
|
215
262
|
Every inbox carries an arbitrary `metadata` object (string / number / boolean values; ≤256 keys,
|
|
216
263
|
≤256 chars per key and per string value). Set it at create time, read it on every inbox shape, and
|
|
217
|
-
patch it in place with shallow-merge / null-delete semantics
|
|
264
|
+
patch it in place with shallow-merge / null-delete semantics: no delete+recreate.
|
|
218
265
|
|
|
219
266
|
```ts
|
|
220
267
|
// Set metadata at create time. It is echoed back on the inbox record.
|
|
@@ -235,30 +282,57 @@ const updated = await extrovert.inboxes.update(inbox.address, {
|
|
|
235
282
|
console.log(updated.metadata); // { tier: 3, vip: true }
|
|
236
283
|
```
|
|
237
284
|
|
|
238
|
-
Metadata is **project-scoped
|
|
285
|
+
Metadata is **project-scoped**: a key only reads/mutates inboxes in its bound project. Where the
|
|
239
286
|
API accepts a `project_id`, it is an **assertion** that must match the key's bound project (a
|
|
240
287
|
mismatch is a 403), never a selector: the project is always derived from the key. See `whoami` for
|
|
241
288
|
the fixed `org_id` / `project_id` the key is bound to.
|
|
242
289
|
|
|
243
290
|
---
|
|
244
291
|
|
|
245
|
-
## Receiving mail
|
|
292
|
+
## Receiving mail: read, thread, reply
|
|
246
293
|
|
|
247
294
|
```ts
|
|
248
|
-
//
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
295
|
+
// Find a conversation. Pass next_cursor back unchanged to continue.
|
|
296
|
+
const firstPage = await extrovert.threads.search(inbox.address, {
|
|
297
|
+
q: "deployment",
|
|
298
|
+
limit: 25,
|
|
299
|
+
});
|
|
300
|
+
const nextPage = firstPage.next_cursor
|
|
301
|
+
? await extrovert.threads.search(inbox.address, {
|
|
302
|
+
q: "deployment",
|
|
303
|
+
limit: 25,
|
|
304
|
+
cursor: firstPage.next_cursor,
|
|
305
|
+
})
|
|
306
|
+
: undefined;
|
|
307
|
+
|
|
308
|
+
// Read the complete oldest-first conversation before acting.
|
|
309
|
+
const summary = firstPage.items[0];
|
|
310
|
+
if (summary) {
|
|
311
|
+
const thread = await extrovert.threads.get(inbox.address, summary.id);
|
|
312
|
+
const authoredText = thread.messages.map((message) =>
|
|
313
|
+
message.extracted_text ?? message.text
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Recipients, subject, In-Reply-To, and References are derived server-side.
|
|
317
|
+
await extrovert.threads.reply(inbox.address, {
|
|
318
|
+
thread_id: thread.id,
|
|
319
|
+
expected_last_message_id: thread.last_message_id,
|
|
320
|
+
text: "On it — thanks.",
|
|
321
|
+
intent: { summary: "Acknowledge the deployment request." },
|
|
322
|
+
idempotency_key: "deployment-ack-v1",
|
|
323
|
+
});
|
|
256
324
|
}
|
|
257
325
|
```
|
|
258
326
|
|
|
327
|
+
If the thread advances first, `expected_last_message_id` returns a 409: fetch the thread again and
|
|
328
|
+
reconsider the draft. It is an optimistic check at submission, not an atomic lock through delivery.
|
|
329
|
+
|
|
330
|
+
For an inbox-bound style, use `inbox.threads(...)`, `inbox.searchThreads(...)`,
|
|
331
|
+
`inbox.thread(...)`, `inbox.reply(...)`, and `inbox.deleteThread(...)`.
|
|
332
|
+
|
|
259
333
|
---
|
|
260
334
|
|
|
261
|
-
## Webhooks
|
|
335
|
+
## Webhooks: verified inbound, anywhere
|
|
262
336
|
|
|
263
337
|
Register an HMAC-signed, timestamped webhook, then verify deliveries with Web Crypto (works in Node
|
|
264
338
|
and at the edge, no dependency):
|
|
@@ -272,11 +346,11 @@ const webhook = await extrovert.webhooks.register({
|
|
|
272
346
|
url: "https://my-agent.example.com/inbound",
|
|
273
347
|
events: ["message.received"],
|
|
274
348
|
});
|
|
275
|
-
// Store webhook.secret now
|
|
349
|
+
// Store webhook.secret now: it is shown once.
|
|
276
350
|
|
|
277
351
|
// In your handler (Workers / Vercel Edge / Node):
|
|
278
352
|
export async function POST(req: Request) {
|
|
279
|
-
const payload = await req.text(); // raw body
|
|
353
|
+
const payload = await req.text(); // raw body: do not re-serialize
|
|
280
354
|
const ok = await verifyWebhookSignature({
|
|
281
355
|
payload,
|
|
282
356
|
signature: req.headers.get("x-extrovert-signature")!,
|
|
@@ -308,12 +382,12 @@ new Extrovert({
|
|
|
308
382
|
|
|
309
383
|
| Option | Env | Default |
|
|
310
384
|
| ------------ | ----------------------- | ------------------------------------ |
|
|
311
|
-
| `apiKey` | `EXTROVERT_API_KEY`
|
|
385
|
+
| `apiKey` | `EXTROVERT_API_KEY` |: (required for `http` transport) |
|
|
312
386
|
| `baseUrl` | `EXTROVERT_API_BASE_URL` | `https://api.extrovert.dev` |
|
|
313
387
|
| `transport` | (`baseUrl=mock`) | `http` |
|
|
314
|
-
| `timeoutMs`
|
|
388
|
+
| `timeoutMs` |: | `30000` |
|
|
315
389
|
|
|
316
|
-
Idempotency: pass `client_id` to `inboxes.create()` and `idempotency_key` to `send` / `reply`
|
|
390
|
+
Idempotency: pass `client_id` to `inboxes.create()` and `idempotency_key` to `send` / `reply` :
|
|
317
391
|
retries won't duplicate. Cursor pagination: list responses carry `next_cursor`; pass it back as
|
|
318
392
|
`cursor`.
|
|
319
393
|
|
|
@@ -326,15 +400,15 @@ Every non-2xx response throws a typed error extending `ApiError`. Branch on the
|
|
|
326
400
|
```ts
|
|
327
401
|
import {
|
|
328
402
|
ApiError,
|
|
329
|
-
AuthenticationError, // 401
|
|
330
|
-
PermissionError, // 403
|
|
331
|
-
ForbiddenScopeError, // 403 forbidden_scope
|
|
332
|
-
BreadthRequiredError, // 400 breadth_required
|
|
333
|
-
NotFoundError, // 404
|
|
334
|
-
ConflictError, // 409
|
|
335
|
-
ValidationError, // 422
|
|
336
|
-
PaymentRequiredError, // 402
|
|
337
|
-
RateLimitError, // 429
|
|
403
|
+
AuthenticationError, // 401: key missing / expired / revoked
|
|
404
|
+
PermissionError, // 403: scope denied
|
|
405
|
+
ForbiddenScopeError, // 403 forbidden_scope: out of the key's ceiling / non-org key on the wildcard
|
|
406
|
+
BreadthRequiredError, // 400 breadth_required: org key on a bare list must pick a project / "-"
|
|
407
|
+
NotFoundError, // 404: incl. an out-of-ceiling id (never an existence oracle)
|
|
408
|
+
ConflictError, // 409: incl. idempotency_conflict (same key, different body)
|
|
409
|
+
ValidationError, // 422: see err.body.error.details
|
|
410
|
+
PaymentRequiredError, // 402: x402 test-mode challenge in err.paymentRequired
|
|
411
|
+
RateLimitError, // 429: err.retryAfter (seconds)
|
|
338
412
|
ConnectionError, // network failure before a response
|
|
339
413
|
TimeoutError, // request timed out / aborted
|
|
340
414
|
} from "@extrovert.dev/sdk";
|
|
@@ -387,13 +461,24 @@ Maps 1:1 to the Extrovert `/v1` REST contract.
|
|
|
387
461
|
| `inbox.send()` | `POST /v1/inboxes/{addr}/send` |
|
|
388
462
|
| `inbox.messages()` | `GET /v1/inboxes/{addr}/messages` |
|
|
389
463
|
| `inbox.threads()` | `GET /v1/inboxes/{addr}/threads` |
|
|
464
|
+
| `inbox.searchThreads()` | `GET /v1/inboxes/{addr}/threads/search` |
|
|
465
|
+
| `inbox.thread(thread_id)` | `GET /v1/inboxes/{addr}/threads/{thread_id}` |
|
|
466
|
+
| `inbox.reply({ thread_id, ... })` | `POST /v1/inboxes/{addr}/reply` |
|
|
467
|
+
| `inbox.deleteThread(thread_id)` | `DELETE /v1/inboxes/{addr}/threads/{thread_id}` |
|
|
390
468
|
| `inbox.waitForEmail()` | `POST /v1/inboxes/{addr}/wait` |
|
|
391
469
|
| `extrovert.inboxes.update(addr, ...)` | `PATCH /v1/inboxes/{addr}` |
|
|
392
470
|
| `extrovert.messages.get(id)` | `GET /v1/messages/{id}` |
|
|
393
|
-
| `extrovert.
|
|
394
|
-
| `extrovert.threads.
|
|
471
|
+
| `extrovert.threads.list(inbox, ...)` | `GET /v1/inboxes/{inbox}/threads` |
|
|
472
|
+
| `extrovert.threads.search(inbox, ...)` | `GET /v1/inboxes/{inbox}/threads/search` |
|
|
473
|
+
| `extrovert.threads.get(inbox, id)` | `GET /v1/inboxes/{inbox}/threads/{id}` |
|
|
474
|
+
| `extrovert.threads.reply(inbox, ...)` | `POST /v1/inboxes/{inbox}/reply` |
|
|
475
|
+
| `extrovert.threads.delete(inbox, id)` | `DELETE /v1/inboxes/{inbox}/threads/{id}` |
|
|
395
476
|
| `extrovert.webhooks.register()` | `POST /v1/webhooks` |
|
|
396
477
|
| `extrovert.domains.onboard(...)` | `POST /v1/domains` |
|
|
478
|
+
| `extrovert.commerce.quoteDomain(...)` | `POST /v1/commerce/domain-quotes` |
|
|
479
|
+
| `extrovert.commerce.requestDomainPurchase(...)` | `POST /v1/commerce/requests/domain-purchases` |
|
|
480
|
+
| `extrovert.commerce.requestPlanChange(...)` | `POST /v1/commerce/requests/plan-changes` |
|
|
481
|
+
| `extrovert.commerce.get(...)` | `GET /v1/commerce/requests/{id}` |
|
|
397
482
|
| `extrovert.whoami()` | `GET /v1/auth/me` |
|
|
398
483
|
|
|
399
484
|
Helpers: `verifyWebhookSignature`, `parseWebhook`, `extractOtp`, `extractLink`,
|
|
@@ -419,30 +504,32 @@ A key carries a subset of these capability scopes (`whoami().scopes`):
|
|
|
419
504
|
| ----------------- | ---------------------------------------------------------------------- |
|
|
420
505
|
| `mailbox:create` | Create inboxes. |
|
|
421
506
|
| `mailbox:read` | Read inboxes, messages, threads. |
|
|
507
|
+
| `mailbox:credentials` | Export raw IMAP/SMTP credentials on paid plans; never implied by read. |
|
|
422
508
|
| `mailbox:send` | Send / reply / forward. |
|
|
423
509
|
| `mailbox:quota` | Change an inbox's effective daily recipient cap (opt-in). |
|
|
424
510
|
| `mailbox:delete` | Delete inboxes. |
|
|
425
511
|
| `webhook:write` | Register / manage webhooks. |
|
|
426
|
-
| `domain:manage` |
|
|
427
|
-
| `domain:
|
|
512
|
+
| `domain:manage` | Onboard / verify / offboard shared or customer-controlled domains. It never buys a domain. |
|
|
513
|
+
| `domain:read` | Read accessible domains, readiness, and domain updates without permission to change setup. |
|
|
514
|
+
| `commerce:request` | Quote and request a domain purchase or plan change, then poll status. It never approves or spends directly. |
|
|
428
515
|
| `review:act` | The BYO reviewer decision plane. |
|
|
429
516
|
|
|
430
517
|
> The `mailbox:*` scope strings are the live wire contract (the public product term is **inbox**;
|
|
431
|
-
> the scope strings are kept verbatim so already-
|
|
518
|
+
> the scope strings are kept verbatim so already-issued keys stay valid).
|
|
432
519
|
|
|
433
520
|
---
|
|
434
521
|
|
|
435
|
-
## Review Loop
|
|
522
|
+
## Review Loop: the open contract
|
|
436
523
|
|
|
437
524
|
The **Review Loop** (HITL) adds supervised autonomy: an agent submits a draft `mode:"review"`,
|
|
438
525
|
a human approves / edits / rejects it, and the loop learns. The stable agent-facing JSON shapes
|
|
439
|
-
are published here as a documented, **versioned open contract
|
|
526
|
+
are published here as a documented, **versioned open contract**: an SDK + skill contract, **not**
|
|
440
527
|
a wire protocol (there is no `/v1/contract` endpoint).
|
|
441
528
|
|
|
442
529
|
```ts
|
|
443
530
|
import { CONTRACT_VERSION, CONTRACT_MANIFEST } from "@extrovert.dev/sdk";
|
|
444
531
|
|
|
445
|
-
CONTRACT_VERSION; // "0.1.0-pre.
|
|
532
|
+
CONTRACT_VERSION; // "0.1.0-pre.7": provisional, pre-1.0; pin it
|
|
446
533
|
CONTRACT_MANIFEST.stability; // "provisional"
|
|
447
534
|
CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","Rule","ReviewEvent"]
|
|
448
535
|
```
|
|
@@ -451,8 +538,8 @@ CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","R
|
|
|
451
538
|
`org_id` / `project_id`. `org` rules are house-style inherited by every project in the org;
|
|
452
539
|
`project` rules are layered on top and outrank broader org rules in the ordered `get_rules`
|
|
453
540
|
precedence ladder. An agent-plane `rules.save(...)` is **always project-layer** (bound to the key's
|
|
454
|
-
project); an agent **cannot author `rule_layer: "org"` rules in v1
|
|
455
|
-
action. (`scope: "general"` still means a house-style rule *within* the project layer
|
|
541
|
+
project); an agent **cannot author `rule_layer: "org"` rules in v1**: that is a console/admin
|
|
542
|
+
action. (`scope: "general"` still means a house-style rule *within* the project layer: `scope` is
|
|
456
543
|
the category axis, `rule_layer` is the ownership axis.)
|
|
457
544
|
|
|
458
545
|
Full reference: the [agent contract](https://docs.extrovert.dev/review-loop/agent-contract/) docs
|
|
@@ -460,21 +547,21 @@ page and the agent skills (`extrovert-send-email`, `extrovert-writing-rules`).
|
|
|
460
547
|
|
|
461
548
|
### Contract & versioning
|
|
462
549
|
|
|
463
|
-
The Review-Loop shapes are an **open, documented contract
|
|
550
|
+
The Review-Loop shapes are an **open, documented contract: versioned *with* this SDK** (not a wire
|
|
464
551
|
protocol; there is no `/v1/contract` endpoint). Three guarantees:
|
|
465
552
|
|
|
466
|
-
- **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.
|
|
553
|
+
- **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.7`**, reconciled across the SDK package
|
|
467
554
|
version, the MCP server, and the OpenAPI `info.version`. Pin it; pin `CONTRACT_MANIFEST` for the
|
|
468
555
|
exact shape set you built against.
|
|
469
|
-
- **Named, documented types.** The five canonical shapes
|
|
470
|
-
`DiffJson` (+`DiffHunk`), `Rule`, `ReviewEvent
|
|
556
|
+
- **Named, documented types.** The five canonical shapes: `ReviewIntent`, `ReviewFeedback`,
|
|
557
|
+
`DiffJson` (+`DiffHunk`), `Rule`, `ReviewEvent`: plus the full M1–M8 surface (submit/states,
|
|
471
558
|
chat, categories, graduation + risk dial, reconciliation + pacing, rules + audit, the BYO reviewer
|
|
472
559
|
decision plane) are exported as named TypeScript types from the `contract` module.
|
|
473
560
|
`CONTRACT_MANIFEST.shapes` enumerates them all by name.
|
|
474
561
|
- **Drift-proof.** A conformance/drift test validates the canonical example JSON against **both** the
|
|
475
562
|
OpenAPI component schemas (Go) and these SDK types, and asserts the version is reconciled across
|
|
476
563
|
every surface (plus a negative test so the guard isn't a tautology). Rename a field anywhere and
|
|
477
|
-
the build breaks
|
|
564
|
+
the build breaks: the published types can't silently diverge from the wire.
|
|
478
565
|
|
|
479
566
|
> **Provisional 0.x.** The contract is open and documented but MAY still evolve additively before
|
|
480
567
|
> 1.0 (no external users yet). Pin `CONTRACT_VERSION` and `CONTRACT_MANIFEST`.
|
|
@@ -483,7 +570,7 @@ protocol; there is no `/v1/contract` endpoint). Three guarantees:
|
|
|
483
570
|
|
|
484
571
|
## Examples
|
|
485
572
|
|
|
486
|
-
Runnable with [`tsx`](https://github.com/privatenumber/tsx)
|
|
573
|
+
Runnable with [`tsx`](https://github.com/privatenumber/tsx): work offline out of the box:
|
|
487
574
|
|
|
488
575
|
```bash
|
|
489
576
|
EXTROVERT_API_BASE_URL=mock npx tsx examples/mailbox-in-one-call.ts
|
|
@@ -494,7 +581,7 @@ EXTROVERT_API_BASE_URL=mock npx tsx examples/wait-for-otp.ts
|
|
|
494
581
|
|
|
495
582
|
## Status
|
|
496
583
|
|
|
497
|
-
> **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.
|
|
584
|
+
> **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.7`: a
|
|
498
585
|
> deliberate **prerelease**, pre-1.0, expect additive change. The offline `mock` transport models the
|
|
499
586
|
> live server closely enough to reproduce a 422 `intent_required` and a queued review, so build and
|
|
500
587
|
> test against it before you have a key. Install from the `next` tag until a stable release is cut.
|