@extrovert.dev/sdk 0.1.0-pre.6 → 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/dist/index.js CHANGED
@@ -94,7 +94,7 @@ var ReviewConflictError = class extends ConflictError {
94
94
  }
95
95
  /**
96
96
  * Whether retrying the same call could ever succeed. False for every subclass
97
- * except {@link StaleError} and {@link BornStaleError} and true there only
97
+ * except {@link StaleError} and {@link BornStaleError} - and true there only
98
98
  * after re-reading and re-applying on top of the other party's change.
99
99
  */
100
100
  get isRetryable() {
@@ -266,7 +266,7 @@ var CURRENT_API_VERSION = "2026-06-23";
266
266
  var API_VERSION_HEADER = "Extrovert-Version";
267
267
 
268
268
  // src/http.ts
269
- var SDK_VERSION = "0.1.0-pre.6";
269
+ var SDK_VERSION = "0.1.0-pre.7";
270
270
  function buildUrl(baseUrl, path, query) {
271
271
  const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
272
272
  const rel = path.startsWith("/") ? path : `/${path}`;
@@ -640,7 +640,23 @@ function mailboxQuickstart(address) {
640
640
  }
641
641
  };
642
642
  }
643
- var SHARED_SUBDOMAIN = "smtp.extrovert.dev";
643
+ var PAID_SHARED_DOMAIN = "extrovertmail.com";
644
+ var FREE_SHARED_DOMAIN = "free.extrovertmail.com";
645
+ var RESERVED_SHARED_LOCAL_PARTS = /* @__PURE__ */ new Set([
646
+ "postmaster",
647
+ "admin",
648
+ "webadmin",
649
+ "legal",
650
+ "fraudmark",
651
+ "fraudmarc",
652
+ "keith",
653
+ "melissa",
654
+ "richard",
655
+ "sydney",
656
+ "syd",
657
+ "john",
658
+ "johnny"
659
+ ]);
644
660
  var MOCK_ORG_ID = "org_mock";
645
661
  var MOCK_PROJECT_ID = "prj_mock";
646
662
  var DEFAULT_DAILY_SEND_LIMIT = 75;
@@ -657,6 +673,13 @@ function rid(prefix) {
657
673
  function randomHandle() {
658
674
  return `agent${Math.floor(1e3 + Math.random() * 9e3)}`;
659
675
  }
676
+ function validatedSharedLocalPart(value) {
677
+ const normalized = value.toLowerCase().trim().replace(/[^a-z0-9._-]/g, "").replace(/^[._-]+|[._-]+$/g, "").slice(0, 40);
678
+ if (normalized.length < 5 || RESERVED_SHARED_LOCAL_PARTS.has(normalized)) {
679
+ throw new ValidationError({ status: 400, code: "invalid", message: "Shared-domain usernames must normalize to at least 5 characters and cannot use a reserved name." });
680
+ }
681
+ return normalized;
682
+ }
660
683
  function assertProjectMatch(projectId) {
661
684
  if (projectId !== void 0 && projectId !== MOCK_PROJECT_ID) {
662
685
  throw new PermissionError({
@@ -745,6 +768,8 @@ function freshState() {
745
768
  contactLists: /* @__PURE__ */ new Map(),
746
769
  domains: /* @__PURE__ */ new Map(),
747
770
  jobs: /* @__PURE__ */ new Map(),
771
+ commerceRequests: /* @__PURE__ */ new Map(),
772
+ commerceByIdempotency: /* @__PURE__ */ new Map(),
748
773
  suppressions: seedSuppressions(),
749
774
  orgReviewPolicy: "require_review",
750
775
  inboxReviewPolicy: /* @__PURE__ */ new Map(),
@@ -875,7 +900,7 @@ function reviewConflict(code, review, detail) {
875
900
  }
876
901
  } else {
877
902
  fields.push(
878
- { field: "revision", code: String(review.revision), detail: "current revision \u2014 use as parent_revision" },
903
+ { field: "revision", code: String(review.revision), detail: "current revision - use as parent_revision" },
879
904
  { field: "version", code: String(review.version), detail: "current row version" }
880
905
  );
881
906
  }
@@ -934,7 +959,7 @@ var MockBackend = class {
934
959
  agent_id: agentId,
935
960
  agent_key: `pk_agent_proj_${agentId.slice(4)}_${rid("sk").slice(3)}`,
936
961
  scopes: ["mailbox:create", "mailbox:read", "mailbox:send", "webhook:write"],
937
- // The minted key is bound to the token's resolved org/project; the agent cannot change it.
962
+ // The issued key is bound to the token's resolved org/project; the agent cannot change it.
938
963
  org_id: MOCK_ORG_ID,
939
964
  project_id: MOCK_PROJECT_ID
940
965
  };
@@ -949,7 +974,7 @@ var MockBackend = class {
949
974
  const existing = this.state.signupByEmail.get(email);
950
975
  const customerId = existing?.customerId ?? `cus_pn_signup_${rid("c").slice(2)}`;
951
976
  const agentId = existing?.agentId ?? rid("agt");
952
- const address = existing?.address ?? `${req.username ?? randomHandle()}@smtp.extrovert.dev`;
977
+ const address = existing?.address ?? `${validatedSharedLocalPart(req.username ?? randomHandle())}@${FREE_SHARED_DOMAIN}`;
953
978
  const otp = "492013";
954
979
  this.state.signupByEmail.set(email, { customerId, agentId, address, otp, verified: false });
955
980
  return {
@@ -1005,8 +1030,10 @@ var MockBackend = class {
1005
1030
  }
1006
1031
  }
1007
1032
  const metadata = req.metadata ? mergeMetadata({}, req.metadata) : {};
1008
- const username = req.username ?? randomHandle();
1009
- const domain = req.domain ?? SHARED_SUBDOMAIN;
1033
+ const domain = req.domain ?? PAID_SHARED_DOMAIN;
1034
+ const normalizedDomain = domain.trim().toLowerCase();
1035
+ const isSharedDomain = normalizedDomain === PAID_SHARED_DOMAIN || normalizedDomain === FREE_SHARED_DOMAIN;
1036
+ const username = isSharedDomain ? validatedSharedLocalPart(req.username ?? randomHandle()) : req.username ?? randomHandle();
1010
1037
  const id = rid("ibx");
1011
1038
  const inbox = {
1012
1039
  object: "inbox",
@@ -1021,6 +1048,7 @@ var MockBackend = class {
1021
1048
  onboarding_mode: req.domain ? "ns_delegated" : "shared",
1022
1049
  agent_id: null,
1023
1050
  daily_send_limit: DEFAULT_DAILY_SEND_LIMIT,
1051
+ direct_smtp_enabled: false,
1024
1052
  webhook_url: req.webhook_url ?? null,
1025
1053
  metadata,
1026
1054
  created_at: now(),
@@ -1059,7 +1087,7 @@ var MockBackend = class {
1059
1087
  * Normalize an inbox ref (opaque id OR address alias) to the canonical address the
1060
1088
  * mock keys its message/thread/contact maps on. The SDK now routes inbox ops by the
1061
1089
  * canonical opaque `id` when it holds a full record (matching the contract's
1062
- * canonical-key semantics), so the mock must resolve an id back to its address
1090
+ * canonical-key semantics), so the mock must resolve an id back to its address -
1063
1091
  * both key `state.inboxes` (same object), `state.messages` keys by address only.
1064
1092
  * Unknown refs pass through unchanged so the existing not-found paths still fire.
1065
1093
  */
@@ -1258,8 +1286,8 @@ var MockBackend = class {
1258
1286
  }
1259
1287
  /**
1260
1288
  * Submit a new message for review (mock). Rides the SAME endpoint as `send` on
1261
- * the real server, so it is literally the same call here: the resolved policy
1262
- * not which SDK method you picked decides whether the message is queued
1289
+ * the real server, so it is literally the same call here: the resolved policy -
1290
+ * not which SDK method you picked - decides whether the message is queued
1263
1291
  * (`kind:"queued_for_review"`) or delivered.
1264
1292
  */
1265
1293
  submitForReview(address, req) {
@@ -1341,7 +1369,7 @@ var MockBackend = class {
1341
1369
  this.state.reviews.set(review.id, review);
1342
1370
  this.enqueueTerminalNudge(review);
1343
1371
  }
1344
- /** Raw delivery for a send no policy, only reachable from submitOutbound. */
1372
+ /** Raw delivery for a send - no policy, only reachable from submitOutbound. */
1345
1373
  deliverSend(address, req) {
1346
1374
  return this.deliverRaw(address, {
1347
1375
  to: toArray(req.to),
@@ -1353,7 +1381,7 @@ var MockBackend = class {
1353
1381
  attachments: req.attachments
1354
1382
  });
1355
1383
  }
1356
- /** Raw delivery for a reply no policy, only reachable from submitOutbound. */
1384
+ /** Raw delivery for a reply - no policy, only reachable from submitOutbound. */
1357
1385
  deliverReply(address, req, env) {
1358
1386
  return this.deliverRaw(address, {
1359
1387
  to: env.to,
@@ -1391,6 +1419,13 @@ var MockBackend = class {
1391
1419
  * the server does before it writes the review row.
1392
1420
  */
1393
1421
  deriveReplyEnvelope(address, req) {
1422
+ if (Boolean(req.thread_id) === Boolean(req.message_id)) {
1423
+ throw new ValidationError({
1424
+ status: 400,
1425
+ code: "bad_request",
1426
+ message: "provide exactly one of thread_id or message_id"
1427
+ });
1428
+ }
1394
1429
  const all = this.state.messages.get(address) ?? [];
1395
1430
  let parent;
1396
1431
  let threadId = req.thread_id;
@@ -1408,6 +1443,13 @@ var MockBackend = class {
1408
1443
  message: "thread_id or message_id is required"
1409
1444
  });
1410
1445
  }
1446
+ if (req.thread_id && req.expected_last_message_id && parent?.id !== req.expected_last_message_id) {
1447
+ throw new ConflictError({
1448
+ status: 409,
1449
+ code: "conflict",
1450
+ message: `thread advanced; latest message is ${parent?.id ?? "unknown"}`
1451
+ });
1452
+ }
1411
1453
  const to = [];
1412
1454
  if (parent) {
1413
1455
  to.push(parent.from.email);
@@ -1666,7 +1708,7 @@ var MockBackend = class {
1666
1708
  * review id so a test can drive the reviewer decision plane offline. `createdAtMs`
1667
1709
  * (optional) backdates created_at so a test can trip the hard review_deadline breaker.
1668
1710
  */
1669
- seedReviewerHeldReview(opts = { fromAddress: "rep@smtp.extrovert.dev" }) {
1711
+ seedReviewerHeldReview(opts = { fromAddress: "reviewer@extrovertmail.com" }) {
1670
1712
  const review = this.createReviewRecord(opts.fromAddress, {
1671
1713
  kind: "send",
1672
1714
  subject: "Pilot proposal",
@@ -1772,7 +1814,7 @@ var MockBackend = class {
1772
1814
  // ---- Category registry (Review Loop, D9/D10) --------------------------
1773
1815
  /**
1774
1816
  * Browse the registry (mock), newest-first, excluding merged/soft-deleted. `match`
1775
- * is a pure lexical filter (every token must appear in name+description) NO LLM,
1817
+ * is a pure lexical filter (every token must appear in name+description) - NO LLM,
1776
1818
  * mirroring the server.
1777
1819
  */
1778
1820
  listCategories(params = {}) {
@@ -1813,7 +1855,7 @@ var MockBackend = class {
1813
1855
  this.state.categories.set(cat.id, cat);
1814
1856
  return cat;
1815
1857
  }
1816
- /** Rename / re-describe a category (mock) metadata only (D10). */
1858
+ /** Rename / re-describe a category (mock) - metadata only (D10). */
1817
1859
  updateCategory(categoryId, req) {
1818
1860
  const cat = this.state.categories.get(categoryId);
1819
1861
  if (!cat) return void 0;
@@ -1823,7 +1865,7 @@ var MockBackend = class {
1823
1865
  this.state.categories.set(cat.id, cat);
1824
1866
  return cat;
1825
1867
  }
1826
- // ---- Graduation + risk dial (Review Loop, D16/D6/D17) agent READ + PROPOSE --
1868
+ // ---- Graduation + risk dial (Review Loop, D16/D6/D17) - agent READ + PROPOSE --
1827
1869
  /** The mock account-default risk dial (mirrors the server defaults). */
1828
1870
  accountDial() {
1829
1871
  return {
@@ -1839,7 +1881,7 @@ var MockBackend = class {
1839
1881
  /**
1840
1882
  * Read the effective risk dial (mock): the account default + every category with an
1841
1883
  * inherited (null override) effective dial. The mock category carries no overrides,
1842
- * so every category inherits effective == account.
1884
+ * so every category inherits - effective == account.
1843
1885
  */
1844
1886
  getRiskDial() {
1845
1887
  const account = this.accountDial();
@@ -1893,7 +1935,7 @@ var MockBackend = class {
1893
1935
  }
1894
1936
  /**
1895
1937
  * Propose graduating a category (mock): returns the current gate status without
1896
- * changing the category state (D16 an agent can never flip the bit).
1938
+ * changing the category state (D16 - an agent can never flip the bit).
1897
1939
  */
1898
1940
  proposeGraduation(categoryId, _req) {
1899
1941
  return this.getGraduationStatus(categoryId);
@@ -1902,7 +1944,7 @@ var MockBackend = class {
1902
1944
  * Read the D19/§8 backlog-reconciliation status (mock): counts the QUEUED drafts in a
1903
1945
  * category that are stale vs current-enough against the current rules-version. The
1904
1946
  * mock has no per-draft composed_* stamps on its Review fixtures, so every queued
1905
- * draft reads as current-enough (composed 0 vs current 0) the contract shape is
1947
+ * draft reads as current-enough (composed 0 vs current 0) - the contract shape is
1906
1948
  * exercised; the integer-compare logic is covered by the Go tests.
1907
1949
  */
1908
1950
  getScanBacklogStatus(categoryId) {
@@ -1926,7 +1968,7 @@ var MockBackend = class {
1926
1968
  };
1927
1969
  }
1928
1970
  /**
1929
- * Read the demand-driven pacing state (mock M7 Slice B/§8): the cursor + effective
1971
+ * Read the demand-driven pacing state (mock - M7 Slice B/§8): the cursor + effective
1930
1972
  * window/ceiling/interval + each queued draft's classification. The mock has no cursor
1931
1973
  * (nothing reviewed) and no composed_* stamps, so every queued draft reads in-window-
1932
1974
  * fresh until the window fills, then ahead; the contract shape is exercised (the
@@ -1966,7 +2008,7 @@ var MockBackend = class {
1966
2008
  const human = r.author_kind === "human" ? 1 : 0;
1967
2009
  return [hard, spec, human, r.rev, r.priority];
1968
2010
  }
1969
- /** Get the ORDERED active rule set (mock) §7 ladder + category-before-general. */
2011
+ /** Get the ORDERED active rule set (mock) - §7 ladder + category-before-general. */
1970
2012
  getRules(params = {}) {
1971
2013
  const active = [...this.state.rules.values()].filter((r) => r.status === "active");
1972
2014
  const byRank = (a, b) => {
@@ -1998,7 +2040,7 @@ var MockBackend = class {
1998
2040
  composition_token_expires_at: params.scope ? void 0 : new Date(Date.now() + 6e5).toISOString()
1999
2041
  };
2000
2042
  }
2001
- /** Save / edit a rule (mock) append-only by supersession (D11). */
2043
+ /** Save / edit a rule (mock) - append-only by supersession (D11). */
2002
2044
  saveRule(req) {
2003
2045
  const text = req.rule_text.trim();
2004
2046
  if (!text) {
@@ -2087,7 +2129,7 @@ var MockBackend = class {
2087
2129
  this.recordRuleAudit("supersede", next.id, ruleSnapshotJSON(prior), ruleSnapshotJSON(next));
2088
2130
  return next;
2089
2131
  }
2090
- /** Retire a rule (mock) soft delete, or undefined when unknown. */
2132
+ /** Retire a rule (mock) - soft delete, or undefined when unknown. */
2091
2133
  retireRule(ruleId) {
2092
2134
  const rule = this.state.rules.get(ruleId);
2093
2135
  if (!rule) return void 0;
@@ -2105,7 +2147,7 @@ var MockBackend = class {
2105
2147
  if (params.entity_id) items = items.filter((e) => e.entity_id === params.entity_id);
2106
2148
  return { items, total: items.length };
2107
2149
  }
2108
- /** Undo a rule change (mock) restore the prior version; idempotent (re-undo 409). */
2150
+ /** Undo a rule change (mock) - restore the prior version; idempotent (re-undo 409). */
2109
2151
  undoRuleChange(udoId) {
2110
2152
  const entry = this.state.ruleAudit.get(udoId);
2111
2153
  if (!entry) throw new NotFoundError({ status: 404, code: "not_found", message: "audit row not found" });
@@ -2209,7 +2251,7 @@ var MockBackend = class {
2209
2251
  }
2210
2252
  }
2211
2253
  /**
2212
- * Enqueue `front_run_next` the signal that the review reached a terminal state
2254
+ * Enqueue `front_run_next` - the signal that the review reached a terminal state
2213
2255
  * while the agent was still trying to act on it.
2214
2256
  *
2215
2257
  * Deduped on (review, terminal state, parent revision) so a retry loop hitting
@@ -2250,8 +2292,8 @@ var MockBackend = class {
2250
2292
  }
2251
2293
  /**
2252
2294
  * Mock-only: mirror an approved draft whose delivery then FAILED at the provider.
2253
- * This is the case the composing agent was previously never told about the
2254
- * console showed the error and the agent's queue stayed silent so the loop test
2295
+ * This is the case the composing agent was previously never told about - the
2296
+ * console showed the error and the agent's queue stayed silent - so the loop test
2255
2297
  * that matters most drives this path.
2256
2298
  */
2257
2299
  simulateSendFailed(reviewId, error = "provider rejected the message") {
@@ -2336,7 +2378,7 @@ var MockBackend = class {
2336
2378
  }
2337
2379
  /**
2338
2380
  * Long-poll for a review event (mock). Offline there is nothing to wait FOR, so it
2339
- * returns the immediate drain (empty when caught up) the server's "empty on
2381
+ * returns the immediate drain (empty when caught up) - the server's "empty on
2340
2382
  * timeout" contract.
2341
2383
  */
2342
2384
  waitForReviewEvent(params = {}) {
@@ -2425,7 +2467,7 @@ var MockBackend = class {
2425
2467
  html: opts.html ?? null,
2426
2468
  extracted_text: opts.text.trim() || null,
2427
2469
  extracted_html: opts.html?.trim() || null,
2428
- message_id: `<${id}@${address.split("@")[1] ?? SHARED_SUBDOMAIN}>`,
2470
+ message_id: `<${id}@${address.split("@")[1] ?? PAID_SHARED_DOMAIN}>`,
2429
2471
  folder: opts.direction === "inbound" ? "INBOX" : "Sent",
2430
2472
  seen: opts.direction === "outbound",
2431
2473
  date: now()
@@ -2541,10 +2583,10 @@ var MockBackend = class {
2541
2583
  if (offset + page.length < total) result.next_cursor = String(offset + page.length);
2542
2584
  return result;
2543
2585
  }
2544
- listThreads(address) {
2586
+ listThreads(address, params = {}) {
2545
2587
  address = this.addrOf(address);
2546
2588
  const items = this.threadsFor(address);
2547
- return { items, total: items.length };
2589
+ return this.paginateThreads(items, params);
2548
2590
  }
2549
2591
  /** Thread-level search (subject / snippet / participant substring). */
2550
2592
  searchThreads(address, params) {
@@ -2553,7 +2595,17 @@ var MockBackend = class {
2553
2595
  const items = this.threadsFor(address).filter(
2554
2596
  (t) => t.subject.toLowerCase().includes(q) || t.snippet.toLowerCase().includes(q) || t.participants.join(" ").toLowerCase().includes(q)
2555
2597
  );
2556
- return { items, total: items.length };
2598
+ return this.paginateThreads(items, params);
2599
+ }
2600
+ paginateThreads(items, params) {
2601
+ const total = items.length;
2602
+ const cursorOffset = params.cursor === void 0 ? void 0 : Number(params.cursor);
2603
+ const offset = params.offset ?? (Number.isFinite(cursorOffset) ? cursorOffset : 0);
2604
+ const limit = params.limit ?? 25;
2605
+ const page = items.slice(offset, offset + limit);
2606
+ const result = { items: page, total };
2607
+ if (offset + page.length < total) result.next_cursor = String(offset + page.length);
2608
+ return result;
2557
2609
  }
2558
2610
  /** Fetch one thread (with messages, oldest-first) by id under an inbox. */
2559
2611
  getThread(address, threadId) {
@@ -2625,17 +2677,18 @@ var MockBackend = class {
2625
2677
  arr.push(m);
2626
2678
  byThread.set(m.thread_id, arr);
2627
2679
  }
2628
- const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms));
2680
+ const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms, true));
2629
2681
  items.sort((a, b) => b.last_message_at.localeCompare(a.last_message_at));
2630
2682
  return items;
2631
2683
  }
2632
- buildThread(address, id, ms) {
2684
+ buildThread(address, id, ms, summary = false) {
2633
2685
  const sorted = [...ms].sort((a, b) => a.date.localeCompare(b.date));
2634
2686
  const last = sorted[sorted.length - 1];
2635
2687
  const seen = /* @__PURE__ */ new Set();
2636
2688
  const participants = [];
2637
- for (const m of sorted) {
2638
- for (const a of [m.from, ...m.to]) {
2689
+ const participantMessages = summary ? [last] : sorted;
2690
+ for (const m of participantMessages) {
2691
+ for (const a of [m.from, ...m.to, ...m.cc ?? [], ...m.reply_to ?? []]) {
2639
2692
  const key = a.email.toLowerCase();
2640
2693
  if (seen.has(key)) continue;
2641
2694
  seen.add(key);
@@ -2649,7 +2702,10 @@ var MockBackend = class {
2649
2702
  participants,
2650
2703
  message_count: sorted.length,
2651
2704
  last_message_at: last.date,
2652
- snippet: (last.text ?? last.html ?? "").slice(0, 120)
2705
+ snippet: (last.text ?? last.html ?? "").slice(0, 120),
2706
+ unread: !last.seen,
2707
+ last_message_has_attachments: (this.state.attachments.get(last.id)?.length ?? 0) > 0,
2708
+ last_message_id: last.id
2653
2709
  };
2654
2710
  }
2655
2711
  /**
@@ -2809,24 +2865,34 @@ ${text}`)) {
2809
2865
  return this.state.contactLists.delete(entryId);
2810
2866
  }
2811
2867
  // ---- domains (Slice 5) ------------------------------------------------
2812
- /** Onboard a domain, mirroring the server's per-mode record set + status. Idempotent on the name. */
2868
+ /** Add a delegated domain and return only the customer-published nameservers. */
2813
2869
  onboardDomain(req) {
2814
2870
  assertProjectMatch(req.project_id);
2815
2871
  const name = req.domain.trim().toLowerCase();
2816
2872
  const existing = this.state.domains.get(name);
2817
2873
  if (existing) return { ...existing };
2818
- const mode = req.mode ?? "ns_delegated";
2874
+ const mode = "ns_delegated";
2819
2875
  const domain = {
2820
2876
  id: rid("dom"),
2821
2877
  domain: name,
2822
2878
  mode,
2823
- verification_status: mode === "shared" ? "verified" : mode === "manual" ? "pending" : "verifying",
2824
- dkim_status: mode === "shared" ? "configured" : mode === "manual" ? "pending" : "configured",
2825
- shared: mode === "shared",
2879
+ verification_status: "verifying",
2880
+ dkim_status: "configured",
2881
+ shared: false,
2826
2882
  created_at: now(),
2827
- records: mode === "manual" || mode === "ns_delegated" ? domainRecordSet(name) : void 0,
2828
- delegation_ns: mode === "ns_delegated" ? domainDelegationNS(name) : void 0,
2829
- instruction: mode === "shared" ? "Shared domain ready. No DNS changes required." : "Add the records, then trigger verification."
2883
+ delegation_ns: domainDelegationNS(name),
2884
+ instruction: "Add the nameserver entries at your domain provider. We check automatically; use Recheck DNS for an immediate check.",
2885
+ readiness: {
2886
+ status: "waiting_for_dns",
2887
+ label: "Waiting for DNS",
2888
+ summary: "We have not confirmed your nameserver entries yet. Add them at your domain provider; we will finish setup automatically.",
2889
+ reason: "dns_entries_unconfirmed",
2890
+ action_required_by: "customer",
2891
+ next_action: "check_dns_entries",
2892
+ ready_for_inboxes: false,
2893
+ poll_after_seconds: 30,
2894
+ inboxes: { scope: "agent", total: 0, ready: 0, setting_up: 0, needs_attention: 0 }
2895
+ }
2830
2896
  };
2831
2897
  this.state.domains.set(name, domain);
2832
2898
  return { ...domain };
@@ -2874,6 +2940,129 @@ ${text}`)) {
2874
2940
  const job = this.state.jobs.get(jobId);
2875
2941
  return job ? { ...job } : void 0;
2876
2942
  }
2943
+ // ---- commerce (quote/request/status; no human approval mutation) ------
2944
+ quoteDomain(req) {
2945
+ const domain = req.domain.trim().toLowerCase();
2946
+ return {
2947
+ object: "domain_quote",
2948
+ domain,
2949
+ available: !domain.startsWith("unavailable."),
2950
+ currency: "usd",
2951
+ quote_cents: 2500,
2952
+ renewal_cents: 2500,
2953
+ premium: false,
2954
+ quote_expires_at: new Date(Date.now() + 15 * 60 * 1e3).toISOString(),
2955
+ blockers: []
2956
+ };
2957
+ }
2958
+ requestDomainPurchase(req) {
2959
+ const idem = `domain_purchase:${req.idempotency_key.trim()}`;
2960
+ const replayId = this.state.commerceByIdempotency.get(idem);
2961
+ if (replayId) return { ...this.state.commerceRequests.get(replayId) };
2962
+ const quote = this.quoteDomain({ domain: req.domain });
2963
+ const timestamp = now();
2964
+ const id = rid("creq");
2965
+ const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
2966
+ const request = {
2967
+ object: "commerce_request",
2968
+ id,
2969
+ kind: "domain_purchase",
2970
+ state: "awaiting_human_approval",
2971
+ domain: quote.domain,
2972
+ domain_scope: req.scope ?? "org",
2973
+ rationale: req.rationale,
2974
+ currency: quote.currency,
2975
+ quote_cents: quote.quote_cents,
2976
+ renewal_cents: quote.renewal_cents,
2977
+ quote_expires_at: quote.quote_expires_at,
2978
+ auto_renew: req.auto_renew ?? true,
2979
+ blocker_code: "human_approval_required",
2980
+ blockers: [
2981
+ {
2982
+ code: "human_approval_required",
2983
+ message: "A human billing administrator must approve this domain purchase.",
2984
+ manage_url: approvalUrl
2985
+ }
2986
+ ],
2987
+ approval_url: approvalUrl,
2988
+ agent_next_action: "Share approval_url with the human, then poll this request after approval.",
2989
+ retry_safe: true,
2990
+ poll_after_seconds: 10,
2991
+ version: 1,
2992
+ created_at: timestamp,
2993
+ updated_at: timestamp
2994
+ };
2995
+ this.state.commerceRequests.set(id, request);
2996
+ this.state.commerceByIdempotency.set(idem, id);
2997
+ return { ...request };
2998
+ }
2999
+ requestPlanChange(req) {
3000
+ const idem = `plan_change:${req.idempotency_key.trim()}`;
3001
+ const replayId = this.state.commerceByIdempotency.get(idem);
3002
+ if (replayId) return { ...this.state.commerceRequests.get(replayId) };
3003
+ const timestamp = now();
3004
+ const id = rid("creq");
3005
+ const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
3006
+ const request = {
3007
+ object: "commerce_request",
3008
+ id,
3009
+ kind: "plan_change",
3010
+ state: "awaiting_human_approval",
3011
+ target_plan: req.target_plan,
3012
+ current_plan: "developer",
3013
+ rationale: req.rationale,
3014
+ currency: "usd",
3015
+ quote_cents: 0,
3016
+ renewal_cents: 0,
3017
+ auto_renew: true,
3018
+ blocker_code: "human_approval_required",
3019
+ blockers: [
3020
+ {
3021
+ code: "human_approval_required",
3022
+ message: "A human billing administrator must approve this plan change.",
3023
+ manage_url: approvalUrl
3024
+ }
3025
+ ],
3026
+ approval_url: approvalUrl,
3027
+ agent_next_action: "Share approval_url with the human, then poll this request after approval.",
3028
+ retry_safe: true,
3029
+ poll_after_seconds: 10,
3030
+ version: 1,
3031
+ created_at: timestamp,
3032
+ updated_at: timestamp
3033
+ };
3034
+ this.state.commerceRequests.set(id, request);
3035
+ this.state.commerceByIdempotency.set(idem, id);
3036
+ return { ...request };
3037
+ }
3038
+ getCommerceRequest(requestId) {
3039
+ const request = this.state.commerceRequests.get(requestId);
3040
+ return request ? { ...request } : void 0;
3041
+ }
3042
+ cancelCommerceRequest(requestId) {
3043
+ const request = this.state.commerceRequests.get(requestId);
3044
+ if (!request) return void 0;
3045
+ if (!["awaiting_human_approval", "blocked", "approved", "payment_action_required", "payment_failed"].includes(request.state)) {
3046
+ throw new ValidationError({ status: 409, code: "conflict", message: "this request can no longer be cancelled" });
3047
+ }
3048
+ request.state = "cancelled";
3049
+ request.blocker_code = void 0;
3050
+ request.blockers = [];
3051
+ request.agent_next_action = "The request is cancelled. Create a new request only if the purchase is still needed.";
3052
+ request.version += 1;
3053
+ request.updated_at = now();
3054
+ return { ...request };
3055
+ }
3056
+ listCommerceRequests(params = {}) {
3057
+ let items = [...this.state.commerceRequests.values()];
3058
+ items.sort((a, b) => b.created_at.localeCompare(a.created_at));
3059
+ const total = items.length;
3060
+ const offset = params.page ? Math.max(0, Number.parseInt(params.page, 10) || 0) : 0;
3061
+ const limit = Math.max(1, Math.min(params.limit ?? 50, 100));
3062
+ const page = items.slice(offset, offset + limit).map((request) => ({ ...request }));
3063
+ const next = offset + page.length;
3064
+ return { items: page, total, next_cursor: next < total ? String(next) : void 0 };
3065
+ }
2877
3066
  // ---- suppressions (recipient opt-outs / list-unsubscribe) --------------
2878
3067
  /**
2879
3068
  * Pre-check whether the caller's org suppresses a recipient (mirrors
@@ -2917,7 +3106,7 @@ ${text}`)) {
2917
3106
  /**
2918
3107
  * Reject the WHOLE send if ANY recipient has an active org-scope suppression,
2919
3108
  * naming exactly the suppressed addresses (never the scope/origin) so the caller
2920
- * can drop them and retry mirroring the live `recipient_suppressed` (422) path.
3109
+ * can drop them and retry - mirroring the live `recipient_suppressed` (422) path.
2921
3110
  */
2922
3111
  enforceSuppression(recipients) {
2923
3112
  const active = new Set(
@@ -3000,16 +3189,6 @@ function redactSecret(w) {
3000
3189
  const { secret: _omit, ...rest } = w;
3001
3190
  return rest;
3002
3191
  }
3003
- function domainRecordSet(domain) {
3004
- const dkimSuffix = domain.replace(/\./g, "-");
3005
- return [
3006
- { name: domain, type: "MX", value: "smtp.extrovert.dev", priority: 10, ttl: 3600 },
3007
- { name: domain, type: "TXT", value: "v=spf1 include:spf.protection.outlook.com -all", ttl: 3600 },
3008
- { name: `_dmarc.${domain}`, type: "TXT", value: "v=DMARC1; p=none; rua=mailto:dmarc@smtp.extrovert.dev", ttl: 3600 },
3009
- { name: `selector1._domainkey.${domain}`, type: "CNAME", value: `selector1-${dkimSuffix}._domainkey.azurecomm.net`, ttl: 3600 },
3010
- { name: `selector2._domainkey.${domain}`, type: "CNAME", value: `selector2-${dkimSuffix}._domainkey.azurecomm.net`, ttl: 3600 }
3011
- ];
3012
- }
3013
3192
  function domainDelegationNS(domain) {
3014
3193
  return [
3015
3194
  { name: domain, type: "NS", value: "ns1.extrovert.dev", ttl: 300 },
@@ -3393,8 +3572,11 @@ var HttpTransport = class {
3393
3572
  signal
3394
3573
  });
3395
3574
  }
3396
- listDomains(signal) {
3397
- return this.call({ method: "GET", path: "/v1/domains", signal });
3575
+ listDomains(signal, params = {}) {
3576
+ return this.call({ method: "GET", path: "/v1/domains", query: params, signal });
3577
+ }
3578
+ listDomainEvents(domain, params, signal) {
3579
+ return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}/events`, query: params, signal });
3398
3580
  }
3399
3581
  getDomain(domain, signal) {
3400
3582
  return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}`, signal });
@@ -3402,6 +3584,8 @@ var HttpTransport = class {
3402
3584
  onboardDomain(req, signal) {
3403
3585
  return this.call({ method: "POST", path: "/v1/domains", body: req, signal });
3404
3586
  }
3587
+ // Delegated domains perform an immediate authoritative DNS check. Inspect
3588
+ // delegation.status separately from mail readiness; 429 requests may be retried.
3405
3589
  verifyDomain(domain, signal) {
3406
3590
  return this.call({
3407
3591
  method: "POST",
@@ -3426,6 +3610,54 @@ var HttpTransport = class {
3426
3610
  getJob(jobId, signal) {
3427
3611
  return this.call({ method: "GET", path: `/v1/jobs/${encodeURIComponent(jobId)}`, signal });
3428
3612
  }
3613
+ quoteDomain(req, signal) {
3614
+ return this.call({ method: "POST", path: "/v1/commerce/domain-quotes", body: req, signal });
3615
+ }
3616
+ requestDomainPurchase(req, signal) {
3617
+ const { idempotency_key, ...body } = req;
3618
+ return this.call({
3619
+ method: "POST",
3620
+ path: "/v1/commerce/requests/domain-purchases",
3621
+ body,
3622
+ idempotencyKey: idempotency_key,
3623
+ signal
3624
+ });
3625
+ }
3626
+ requestPlanChange(req, signal) {
3627
+ const { idempotency_key, ...body } = req;
3628
+ return this.call({
3629
+ method: "POST",
3630
+ path: "/v1/commerce/requests/plan-changes",
3631
+ body,
3632
+ idempotencyKey: idempotency_key,
3633
+ signal
3634
+ });
3635
+ }
3636
+ getCommerceRequest(requestId, signal) {
3637
+ return this.call({
3638
+ method: "GET",
3639
+ path: `/v1/commerce/requests/${encodeURIComponent(requestId)}`,
3640
+ signal
3641
+ });
3642
+ }
3643
+ cancelCommerceRequest(requestId, signal) {
3644
+ return this.call({
3645
+ method: "POST",
3646
+ path: `/v1/commerce/requests/${encodeURIComponent(requestId)}/cancel`,
3647
+ signal
3648
+ });
3649
+ }
3650
+ listCommerceRequests(params, signal) {
3651
+ return this.call({
3652
+ method: "GET",
3653
+ path: "/v1/commerce/requests",
3654
+ query: {
3655
+ limit: params.limit,
3656
+ page: params.page
3657
+ },
3658
+ signal
3659
+ });
3660
+ }
3429
3661
  submitForReview(address, req, signal) {
3430
3662
  return this.call({
3431
3663
  method: "POST",
@@ -3784,8 +4016,8 @@ var MockTransport = class {
3784
4016
  async searchMessages(address, params) {
3785
4017
  return this.backend.searchMessages(address, params);
3786
4018
  }
3787
- async listThreads(address, _params) {
3788
- return this.backend.listThreads(address);
4019
+ async listThreads(address, params) {
4020
+ return this.backend.listThreads(address, params);
3789
4021
  }
3790
4022
  async searchThreads(address, params) {
3791
4023
  return this.backend.searchThreads(address, params);
@@ -3849,8 +4081,16 @@ var MockTransport = class {
3849
4081
  if (!row) throw notFound("suppression", id);
3850
4082
  return row;
3851
4083
  }
3852
- async listDomains() {
3853
- return this.backend.listDomains();
4084
+ async listDomains(_signal, params = {}) {
4085
+ const page = this.backend.listDomains();
4086
+ const offset = Number(params.page ?? 0);
4087
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid page cursor");
4088
+ const items = page.items.slice(offset, offset + (params.limit ?? 50));
4089
+ return { items, total: page.total, next_cursor: offset + items.length < page.items.length ? String(offset + items.length) : void 0 };
4090
+ }
4091
+ async listDomainEvents(domain, params) {
4092
+ if (!this.backend.getDomain(domain)) throw notFound("domain", domain);
4093
+ return { items: [], next_cursor: params.after ?? "0", has_more: false, poll_after_seconds: 30 };
3854
4094
  }
3855
4095
  async getDomain(domain) {
3856
4096
  const d = this.backend.getDomain(domain);
@@ -3875,6 +4115,28 @@ var MockTransport = class {
3875
4115
  if (!job) throw notFound("job", jobId);
3876
4116
  return job;
3877
4117
  }
4118
+ async quoteDomain(req) {
4119
+ return this.backend.quoteDomain(req);
4120
+ }
4121
+ async requestDomainPurchase(req) {
4122
+ return this.backend.requestDomainPurchase(req);
4123
+ }
4124
+ async requestPlanChange(req) {
4125
+ return this.backend.requestPlanChange(req);
4126
+ }
4127
+ async getCommerceRequest(requestId) {
4128
+ const request = this.backend.getCommerceRequest(requestId);
4129
+ if (!request) throw notFound("commerce request", requestId);
4130
+ return request;
4131
+ }
4132
+ async cancelCommerceRequest(requestId) {
4133
+ const request = this.backend.cancelCommerceRequest(requestId);
4134
+ if (!request) throw notFound("commerce request", requestId);
4135
+ return request;
4136
+ }
4137
+ async listCommerceRequests(params) {
4138
+ return this.backend.listCommerceRequests(params);
4139
+ }
3878
4140
  async submitForReview(address, req) {
3879
4141
  return this.backend.submitForReview(address, req);
3880
4142
  }
@@ -4029,6 +4291,49 @@ function filenameFromDisposition(disposition) {
4029
4291
  return m ? decodeURIComponent(m[1].trim()) : "";
4030
4292
  }
4031
4293
 
4294
+ // src/domain-wait.ts
4295
+ async function waitForDomain(get, options = {}) {
4296
+ const seconds = options.timeout_seconds ?? 45;
4297
+ if (!Number.isInteger(seconds) || seconds < 0 || seconds > 50) throw new Error("timeout_seconds must be an integer between 0 and 50");
4298
+ const deadline = AbortSignal.timeout(Math.max(1, seconds * 1e3));
4299
+ const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
4300
+ let latest;
4301
+ const result = (outcome) => ({
4302
+ domain: latest,
4303
+ outcome,
4304
+ resume_after_seconds: outcome === "timed_out" ? Math.max(5, Math.min(60, latest?.readiness?.poll_after_seconds ?? 30)) : 0
4305
+ });
4306
+ try {
4307
+ const singleCheckDeadline = seconds === 0 ? AbortSignal.timeout(1e4) : void 0;
4308
+ latest = await get(singleCheckDeadline ? options.signal ? AbortSignal.any([options.signal, singleCheckDeadline]) : singleCheckDeadline : signal);
4309
+ for (; ; ) {
4310
+ const r = latest.readiness;
4311
+ if (!r) return result("status_unavailable");
4312
+ if (r.ready_for_inboxes) return result("ready");
4313
+ if (r.action_required_by === "customer") return result("action_required");
4314
+ if (r.status === "needs_attention") return result("needs_attention");
4315
+ if (seconds === 0) return result("timed_out");
4316
+ await new Promise((resolve, reject) => {
4317
+ signal.throwIfAborted();
4318
+ const abort = () => {
4319
+ clearTimeout(timer);
4320
+ reject(signal.reason);
4321
+ };
4322
+ const timer = setTimeout(() => {
4323
+ signal.removeEventListener("abort", abort);
4324
+ resolve();
4325
+ }, Math.max(5, Math.min(60, r.poll_after_seconds || 30)) * 1e3);
4326
+ signal.addEventListener("abort", abort, { once: true });
4327
+ });
4328
+ latest = await get(signal);
4329
+ }
4330
+ } catch (error) {
4331
+ if (options.signal?.aborted) throw options.signal.reason;
4332
+ if (deadline.aborted && latest) return result("timed_out");
4333
+ throw error;
4334
+ }
4335
+ }
4336
+
4032
4337
  // src/key-tier.ts
4033
4338
  var AGENT_HEAD = "pk_agent_";
4034
4339
  function parseKeyTier(apiKey) {
@@ -4113,8 +4418,8 @@ var InboxHandle = class {
4113
4418
  * Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
4114
4419
  * human has to approve it and NOTHING has been delivered yet; anything else was
4115
4420
  * delivered. Under the default `require_review` policy a call WITHOUT an
4116
- * `intent` raises `IntentRequiredError` (422) instead nothing sent, nothing
4117
- * queued so pass one, or read `inbox.record.effective_review_policy` first.
4421
+ * `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
4422
+ * queued: so pass one, or read `inbox.record.effective_review_policy` first.
4118
4423
  */
4119
4424
  send(req, signal) {
4120
4425
  return this.transport.send(this.ref, req, signal);
@@ -4124,7 +4429,7 @@ var InboxHandle = class {
4124
4429
  * the latest message) or `message_id` (reply to that message); the server
4125
4430
  * derives To / Subject / In-Reply-To / References. Set `reply_all` to reply to
4126
4431
  * every thread recipient. Returns the same three-way {@link SendOutcome} as
4127
- * {@link send} a reply is governed by the review policy too.
4432
+ * {@link send}: a reply is governed by the review policy too.
4128
4433
  */
4129
4434
  reply(req, signal) {
4130
4435
  return this.transport.reply(this.ref, req, signal);
@@ -4133,7 +4438,7 @@ var InboxHandle = class {
4133
4438
  * Forward a message in this inbox to new recipients, preserving the original.
4134
4439
  *
4135
4440
  * A forward is an outbound message to arbitrary NEW recipients that quotes an
4136
- * inbound thread, so it is governed by the review policy exactly like a send
4441
+ * inbound thread, so it is governed by the review policy exactly like a send :
4137
4442
  * same {@link SendOutcome} union, same `intent` requirement.
4138
4443
  */
4139
4444
  forward(messageId, req, signal) {
@@ -4297,7 +4602,7 @@ var ListPage = class _ListPage {
4297
4602
  this.nextCursor = raw.next_cursor;
4298
4603
  }
4299
4604
  /**
4300
- * Fetch the next page. Throws if there is none guard with {@link hasMore}.
4605
+ * Fetch the next page. Throws if there is none - guard with {@link hasMore}.
4301
4606
  */
4302
4607
  async nextPage(signal) {
4303
4608
  if (!this.hasMore || this.nextCursor === null) {
@@ -4385,8 +4690,8 @@ var ProjectInboxes = class {
4385
4690
  * Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
4386
4691
  * human has to approve it and NOTHING has been delivered yet; anything else was
4387
4692
  * delivered. Under the default `require_review` policy a call WITHOUT an
4388
- * `intent` raises `IntentRequiredError` (422) instead nothing sent, nothing
4389
- * queued so pass one, or read `inbox.record.effective_review_policy` first.
4693
+ * `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
4694
+ * queued: so pass one, or read `inbox.record.effective_review_policy` first.
4390
4695
  */
4391
4696
  send(projectId, inboxId, req, signal) {
4392
4697
  return this.ctx.transport.send(this.ref(projectId, inboxId), req, signal);
@@ -4452,14 +4757,14 @@ var ProjectInboxes = class {
4452
4757
  *
4453
4758
  * The frozen contract project-prefixes ONLY the inbox collection/item/credentials
4454
4759
  * routes (`/v1/projects/{project_id}/inboxes[/{inbox_id}][/credentials]`); the
4455
- * send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path
4760
+ * send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path :
4456
4761
  * they address the inbox by its opaque id directly (`/v1/inboxes/{inbox_id}/…`),
4457
4762
  * where the project is implicit in (and enforced by) the inbox id server-side.
4458
4763
  *
4459
4764
  * So for these sub-ops `projectId` cannot be carried on the URL and is NOT a URL
4460
4765
  * selector. The adversarial review flagged that silently discarding it makes the
4461
4766
  * signature misleading. CHOICE: keep the arg (dropping it would break the chain's
4462
- * symmetry with create/list/get/update/delete the more disruptive option) but
4767
+ * symmetry with create/list/get/update/delete: the more disruptive option) but
4463
4768
  * VALIDATE it rather than ignore it. We reject the two client mistakes we can catch
4464
4769
  * without a round-trip:
4465
4770
  * - a blank / whitespace-only `projectId` (a required selector everywhere else in
@@ -4498,8 +4803,9 @@ var Inboxes = class {
4498
4803
  this.ctx = ctx;
4499
4804
  }
4500
4805
  /**
4501
- * Create an inbox. The default path mints an address on a pre-verified shared subdomain of
4502
- * `smtp.extrovert.dev`, so it returns a live, send-and-receive-capable inbox in one call.
4806
+ * Create an inbox. The default path creates an address on `extrovertmail.com`
4807
+ * for paid accounts or `free.extrovertmail.com` for free signups, so it returns
4808
+ * a live inbox in one call.
4503
4809
  *
4504
4810
  * Pass `metadata` to attach arbitrary key-value data, and `client_id` for idempotent creation
4505
4811
  * (re-calling with the same id returns the same inbox, with its metadata replayed verbatim).
@@ -4509,12 +4815,12 @@ var Inboxes = class {
4509
4815
  return new InboxHandle(this.ctx.transport, inbox.address, this.ctx.handleOptions, inbox);
4510
4816
  }
4511
4817
  /**
4512
- * List inboxes visible to the calling key (the bare curl-sugar surface resolves
4818
+ * List inboxes visible to the calling key (the bare curl-sugar surface: resolves
4513
4819
  * to the key's default project). An org-tier key has no single default project, so
4514
4820
  * the bare list is ambiguous: fail fast client-side with a BreadthRequiredError that
4515
4821
  * names the next call, matching the MCP surface, instead of round-tripping to a 400.
4516
4822
  * Use `extrovert.projects.inboxes.list("<project_id>")` or `"-"` (org subtree) for
4517
- * an org key. The check is advisory the server stays authoritative.
4823
+ * an org key. The check is advisory: the server stays authoritative.
4518
4824
  */
4519
4825
  list(params = {}, signal) {
4520
4826
  if (tierNeedsExplicitBreadth(this.ctx.keyTier)) {
@@ -4610,10 +4916,22 @@ var Threads = class {
4610
4916
  constructor(ctx) {
4611
4917
  this.ctx = ctx;
4612
4918
  }
4919
+ /** List conversations newest-active first. Pass `next_cursor` back as `cursor` for the next page. */
4920
+ list(inbox, params = {}, signal) {
4921
+ return this.ctx.transport.listThreads(inbox, params, signal);
4922
+ }
4923
+ /** Search thread subjects, snippets, and participants. Cursor pagination matches {@link list}. */
4924
+ search(inbox, params, signal) {
4925
+ return this.ctx.transport.searchThreads(inbox, params, signal);
4926
+ }
4613
4927
  /** Fetch one thread (+ its messages, oldest-first) by id under its owning inbox address. */
4614
4928
  get(inbox, threadId, signal) {
4615
4929
  return this.ctx.transport.getThread(inbox, threadId, signal);
4616
4930
  }
4931
+ /** Reply in a thread; recipients and RFC reply headers are derived server-side. */
4932
+ reply(inbox, req, signal) {
4933
+ return this.ctx.transport.reply(inbox, req, signal);
4934
+ }
4617
4935
  /**
4618
4936
  * Delete an entire thread (every message): move to Trash (default) or
4619
4937
  * permanently remove when `expunge` is true. `inbox` is the owning address.
@@ -4675,7 +4993,7 @@ var Suppressions = class {
4675
4993
  }
4676
4994
  /**
4677
4995
  * Pre-check whether the caller's org already suppresses a recipient, BEFORE
4678
- * composing. `suppressed: true` means a send to them would be rejected skip
4996
+ * composing. `suppressed: true` means a send to them would be rejected: skip
4679
4997
  * that recipient. Returns the matching org rows too (never a global/shared row).
4680
4998
  */
4681
4999
  precheck(recipient, signal) {
@@ -4699,17 +5017,25 @@ var Domains = class {
4699
5017
  this.ctx = ctx;
4700
5018
  }
4701
5019
  /** List the customer's onboarded domains and their status. */
4702
- list(signal) {
4703
- return this.ctx.transport.listDomains(signal);
5020
+ list(paramsOrSignal = {}, signal) {
5021
+ if ("aborted" in paramsOrSignal) return this.ctx.transport.listDomains(paramsOrSignal);
5022
+ return this.ctx.transport.listDomains(signal, paramsOrSignal);
4704
5023
  }
4705
- /** Get one domain's detail + verification status + the DNS records to set, inline. */
5024
+ /** Get one domain's detail, verification status, and nameserver records. */
4706
5025
  get(domain, signal) {
4707
5026
  return this.ctx.transport.getDomain(domain, signal);
4708
5027
  }
5028
+ /** Wait up to 50 seconds, then return an explicit resumable outcome. No DNS writes. */
5029
+ wait(domain, options = {}) {
5030
+ return waitForDomain((signal) => this.ctx.transport.getDomain(domain, signal), options);
5031
+ }
5032
+ /** Resume durable updates for this domain using the previous next_cursor as after. */
5033
+ events(domain, params = {}, signal) {
5034
+ return this.ctx.transport.listDomainEvents(domain, params, signal);
5035
+ }
4709
5036
  /**
4710
- * Onboard (add) a domain. `mode` defaults to ns_delegated. `mode: "purchased"`
4711
- * requires the `domain:purchase` scope (in addition to `domain:manage`). Returns
4712
- * the record set / NS instruction.
5037
+ * Add a delegated inbox domain the customer controls. Returns the nameserver
5038
+ * records to publish and never spends money.
4713
5039
  */
4714
5040
  onboard(req, signal) {
4715
5041
  return this.ctx.transport.onboardDomain(req, signal);
@@ -4728,6 +5054,46 @@ var Domains = class {
4728
5054
  return this.ctx.transport.offboardDomain(domain, signal);
4729
5055
  }
4730
5056
  };
5057
+ var Commerce = class {
5058
+ constructor(ctx) {
5059
+ this.ctx = ctx;
5060
+ }
5061
+ requireIdempotencyKey(value) {
5062
+ if (value.trim().length < 8) {
5063
+ throw new ValidationError({
5064
+ status: 400,
5065
+ code: "bad_request",
5066
+ message: "idempotency_key must be a stable value of at least 8 characters; reuse it for retries of the same intent."
5067
+ });
5068
+ }
5069
+ }
5070
+ /** Quote a domain without purchasing, reserving, or approving it. */
5071
+ quoteDomain(req, signal) {
5072
+ return this.ctx.transport.quoteDomain(req, signal);
5073
+ }
5074
+ /** Create a durable domain-purchase request for human approval. */
5075
+ requestDomainPurchase(req, signal) {
5076
+ this.requireIdempotencyKey(req.idempotency_key);
5077
+ return this.ctx.transport.requestDomainPurchase(req, signal);
5078
+ }
5079
+ /** Create a durable plan-upgrade or downgrade request for human approval. */
5080
+ requestPlanChange(req, signal) {
5081
+ this.requireIdempotencyKey(req.idempotency_key);
5082
+ return this.ctx.transport.requestPlanChange(req, signal);
5083
+ }
5084
+ /** Poll one request's exact blockers, approval URL, and next-action guidance. */
5085
+ get(requestId, signal) {
5086
+ return this.ctx.transport.getCommerceRequest(requestId, signal);
5087
+ }
5088
+ /** Withdraw this agent's request while its durable state still permits cancellation. */
5089
+ cancel(requestId, signal) {
5090
+ return this.ctx.transport.cancelCommerceRequest(requestId, signal);
5091
+ }
5092
+ /** List visible commerce requests using the API's opaque page token. */
5093
+ list(params = {}, signal) {
5094
+ return this.ctx.transport.listCommerceRequests(params, signal);
5095
+ }
5096
+ };
4731
5097
  var Reviews = class {
4732
5098
  constructor(ctx) {
4733
5099
  this.ctx = ctx;
@@ -4748,7 +5114,7 @@ var Reviews = class {
4748
5114
  /**
4749
5115
  * Get the human's assembled feedback (M5): the diff + comments + decision + the
4750
5116
  * rules born from this review. Read it after a rejected/edited nudge to learn what
4751
- * the human wanted. $0 LLM pure assembly on our side.
5117
+ * the human wanted. $0 LLM: pure assembly on our side.
4752
5118
  */
4753
5119
  feedback(reviewId, signal) {
4754
5120
  return this.ctx.transport.getReviewFeedback(reviewId, signal);
@@ -4756,7 +5122,7 @@ var Reviews = class {
4756
5122
  /**
4757
5123
  * Post a chat turn on a review's thread (M5): an agent question to the human
4758
5124
  * reviewer; flips in_review -> chatting on the first turn. Idempotent on the
4759
- * optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM you compose it.
5125
+ * optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM: you compose it.
4760
5126
  */
4761
5127
  chat(reviewId, req, idempotencyKey, signal) {
4762
5128
  return this.ctx.transport.postReviewChat(reviewId, req, idempotencyKey, signal);
@@ -4764,8 +5130,8 @@ var Reviews = class {
4764
5130
  /**
4765
5131
  * Post a new agent draft under a parent_revision CAS (M5; D17). parent_revision
4766
5132
  * must equal the draft's current revision, else a 409 STALE with NO mutation (the
4767
- * human always wins re-read, re-apply, retry). On success the draft is re-rendered
4768
- * in place (revision++) and returns to needs_review. $0 LLM you compose the redraft.
5133
+ * human always wins: re-read, re-apply, retry). On success the draft is re-rendered
5134
+ * in place (revision++) and returns to needs_review. $0 LLM: you compose the redraft.
4769
5135
  */
4770
5136
  revise(reviewId, req, signal) {
4771
5137
  return this.ctx.transport.submitRevision(reviewId, req, signal);
@@ -4784,9 +5150,9 @@ var Reviews = class {
4784
5150
  * assert "I reviewed this against rules vX and no change is needed", advancing the
4785
5151
  * draft's composed_* versions with no new draft, no revision bump, no nudge. A
4786
5152
  * born-stale draft re-stamped to the current version becomes current-enough and
4787
- * releasable on the next reconciliation sweep the cheap counterpart to revise().
5153
+ * releasable on the next reconciliation sweep: the cheap counterpart to revise().
4788
5154
  * against_version above the category's current rules-version is 400; a terminal draft
4789
- * 409s. $0 LLM you judged.
5155
+ * 409s. $0 LLM: you judged.
4790
5156
  */
4791
5157
  restamp(reviewId, req, signal) {
4792
5158
  return this.ctx.transport.restampReview(reviewId, req, signal);
@@ -4805,13 +5171,13 @@ var Reviews = class {
4805
5171
  }
4806
5172
  /**
4807
5173
  * Submit a reviewer decision (M8 Slice B; reviewer_decide, D5/§9). approve/edit → the
4808
- * PLATFORM ACS-sends with the COMPOSER's credentials (the reviewer NEVER holds
4809
- * mailbox:send on an inbox it doesn't own the credential boundary); reject → back to
5174
+ * PLATFORM sends with the COMPOSER's credentials (the reviewer NEVER holds
5175
+ * mailbox:send on an inbox it doesn't own: the credential boundary); reject → back to
4810
5176
  * the composer (needs_review, hop_count++); escalate → the human queue. revision/
4811
- * version are the CAS (409 STALE on mismatch, NO mutation the human always wins,
5177
+ * version are the CAS (409 STALE on mismatch, NO mutation: the human always wins,
4812
5178
  * D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline)
4813
- * FORCE a reject to the human regardless of intent `forced_by_breaker` names it. $0
4814
- * LLM you judged; we route, send, and enforce the breakers.
5179
+ * FORCE a reject to the human regardless of intent: `forced_by_breaker` names it. $0
5180
+ * LLM: you judged; we route, send, and enforce the breakers.
4815
5181
  */
4816
5182
  decide(reviewId, req, signal) {
4817
5183
  return this.ctx.transport.reviewerDecide(reviewId, req, signal);
@@ -4850,14 +5216,14 @@ var Categories = class {
4850
5216
  propose(req, signal) {
4851
5217
  return this.ctx.transport.proposeCategory(req, signal);
4852
5218
  }
4853
- /** Rename / re-describe a category metadata only (D10). */
5219
+ /** Rename / re-describe a category: metadata only (D10). */
4854
5220
  update(categoryId, req, signal) {
4855
5221
  return this.ctx.transport.updateCategory(categoryId, req, signal);
4856
5222
  }
4857
5223
  /**
4858
5224
  * Read the effective risk dial (D4/D12): the account default + every category's
4859
5225
  * overrides (each with its resolved effective value; null override = inherit).
4860
- * Read-only agents read but NEVER flip the dial; setting it is a human (console)
5226
+ * Read-only: agents read but NEVER flip the dial; setting it is a human (console)
4861
5227
  * action (D16).
4862
5228
  */
4863
5229
  riskDial(signal) {
@@ -4873,7 +5239,7 @@ var Categories = class {
4873
5239
  }
4874
5240
  /**
4875
5241
  * Propose graduating a category (D16/D6): RECORDS the request (durable evidence) and
4876
- * returns the current gate status. It does NOT change the category state flipping
5242
+ * returns the current gate status. It does NOT change the category state: flipping
4877
5243
  * the bit is a human (console) action; an agent only proposes.
4878
5244
  */
4879
5245
  proposeGraduation(categoryId, req = {}, signal) {
@@ -4882,7 +5248,7 @@ var Categories = class {
4882
5248
  /**
4883
5249
  * Read the D19/§8 backlog-reconciliation status: how many of the category's QUEUED
4884
5250
  * drafts are stale vs current-enough against the current rules-version (a pure
4885
- * integer compare, $0 LLM). Read-only you READ the picture; the human (console
5251
+ * integer compare, $0 LLM). Read-only: you READ the picture; the human (console
4886
5252
  * scan-backlog) or the graduate/rule-change hooks TRIGGER the actual reconciliation
4887
5253
  * sweep that releases current-enough drafts and nudges stale ones to redraft.
4888
5254
  */
@@ -4913,7 +5279,7 @@ var Rules = class {
4913
5279
  * Save / edit a rule (append-only by supersession; D11). An agent-plane save is
4914
5280
  * ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
4915
5281
  * key's project. Agents cannot author org-layer / house-style (`rule_layer:"org"`)
4916
- * rules in v1 that is a console/admin action.
5282
+ * rules in v1: that is a console/admin action.
4917
5283
  */
4918
5284
  save(req, signal) {
4919
5285
  return this.ctx.transport.saveRule(req, signal);
@@ -4922,7 +5288,7 @@ var Rules = class {
4922
5288
  promote(ruleId, toScope, signal) {
4923
5289
  return this.ctx.transport.promoteRule(ruleId, toScope, signal);
4924
5290
  }
4925
- /** Retire a rule soft delete; the history survives as training data. */
5291
+ /** Retire a rule: soft delete; the history survives as training data. */
4926
5292
  retire(ruleId, signal) {
4927
5293
  return this.ctx.transport.retireRule(ruleId, signal);
4928
5294
  }
@@ -4930,7 +5296,7 @@ var Rules = class {
4930
5296
  audit(params = {}, signal) {
4931
5297
  return this.ctx.transport.getRuleAudit(params, signal);
4932
5298
  }
4933
- /** Undo a rule change by its audit-row id (udo_…) restore the prior version. */
5299
+ /** Undo a rule change by its audit-row id (udo_…): restore the prior version. */
4934
5300
  undo(udoId, signal) {
4935
5301
  return this.ctx.transport.undoRuleChange(udoId, signal);
4936
5302
  }
@@ -4987,26 +5353,29 @@ var ExtrovertClient = class _ExtrovertClient {
4987
5353
  this.contactLists = new ContactLists(ctx);
4988
5354
  this.suppressions = new Suppressions(ctx);
4989
5355
  this.domains = new Domains(ctx);
5356
+ this.commerce = new Commerce(ctx);
4990
5357
  this.reviews = new Reviews(ctx);
4991
5358
  this.categories = new Categories(ctx);
4992
5359
  this.rules = new Rules(ctx);
4993
5360
  this.projects = new Projects(ctx);
4994
5361
  }
4995
5362
  /**
4996
- * Redeem an enrollment token (`pk_enroll_...`) and mint a scoped agent key.
5363
+ * Redeem an enrollment token (`pk_enroll_...`) and issue a scoped agent key.
4997
5364
  *
4998
5365
  * Idempotent on `agent_handle`: redeeming twice with the same handle returns the same agent.
4999
- * Returns the raw `EnrollResponse` to immediately use the minted key, prefer
5366
+ * Returns the raw `EnrollResponse` - to immediately use the issued key, prefer
5000
5367
  * {@link ExtrovertClient.enrolled}.
5001
5368
  */
5002
5369
  enroll(req, signal) {
5003
5370
  return this.transport.enroll(req, signal);
5004
5371
  }
5005
5372
  /**
5006
- * Grab a free account in one unauthenticated call (Slice E). Provisions a tenant
5007
- * + a first inbox and returns a LIMITED-scope (read-only) agent key; a one-time
5008
- * code is emailed to `human_email`. Call {@link verify} with the code to unlock
5009
- * full scopes. Idempotent on `human_email`: re-calling rotates the key + resends.
5373
+ * Request a free account in one unauthenticated call. When free signup is
5374
+ * enabled, this provisions a tenant plus a first inbox and returns a
5375
+ * verification-only agent key. That key can only call {@link verify}; it cannot
5376
+ * read or send mail. A one-time code is emailed to `human_email`. Call
5377
+ * {@link verify} with the code to activate the account and receive full scopes.
5378
+ * Idempotent on `human_email`: re-calling rotates the key and resends the code.
5010
5379
  * When free signup is paused, this throws an `ApiError` with status 403 and
5011
5380
  * code `signup_disabled` without creating account state.
5012
5381
  */
@@ -5029,7 +5398,7 @@ var ExtrovertClient = class _ExtrovertClient {
5029
5398
  return this.transport.whoami(signal);
5030
5399
  }
5031
5400
  /**
5032
- * Poll the status of an async job (`GET /v1/jobs/{job_id}`) currently only
5401
+ * Poll the status of an async job (`GET /v1/jobs/{job_id}`) - currently only
5033
5402
  * the domain-offboard teardown started by {@link Domains.offboard} enqueues
5034
5403
  * one. `status` is terminal on succeeded/failed/cancelled; keep polling
5035
5404
  * otherwise. An unknown or foreign job id is a {@link NotFoundError}.
@@ -5038,8 +5407,8 @@ var ExtrovertClient = class _ExtrovertClient {
5038
5407
  return this.transport.getJob(jobId, signal);
5039
5408
  }
5040
5409
  /**
5041
- * Redeem an enrollment token and return a *new* client already authenticated with the minted
5042
- * agent key the natural "redeem then act" flow for an agent.
5410
+ * Redeem an enrollment token and return a *new* client already authenticated with the issued
5411
+ * agent key - the natural "redeem then act" flow for an agent.
5043
5412
  *
5044
5413
  * ```ts
5045
5414
  * const bootstrap = new Extrovert({ apiKey: enrollmentToken });
@@ -5060,7 +5429,7 @@ var ExtrovertClient = class _ExtrovertClient {
5060
5429
  return { client, enrollment };
5061
5430
  }
5062
5431
  /**
5063
- * Get an ergonomic handle to an existing inbox by address without an extra round-trip. Use this
5432
+ * Get an ergonomic handle to an existing inbox by address - without an extra round-trip. Use this
5064
5433
  * when you already know the address (e.g. from a previous create) and want to send/wait/reply.
5065
5434
  * Call {@link InboxHandle.refresh} to load the full record.
5066
5435
  */
@@ -5187,14 +5556,14 @@ async function signWebhook(secret, body, timestampSeconds) {
5187
5556
  }
5188
5557
 
5189
5558
  // src/contract.ts
5190
- var CONTRACT_VERSION = "0.1.0-pre.6";
5559
+ var CONTRACT_VERSION = "0.1.0-pre.7";
5191
5560
  var CONTRACT_MANIFEST = {
5192
5561
  name: "extrovert.review-loop",
5193
5562
  version: CONTRACT_VERSION,
5194
5563
  stability: "provisional",
5195
5564
  kind: "sdk+skill-contract",
5196
5565
  spec_ref: "hitl-spec.md#11",
5197
- // §11 core the five canonical example shapes.
5566
+ // §11 core - the five canonical example shapes.
5198
5567
  core_shapes: ["ReviewIntent", "ReviewFeedback", "DiffJson", "Rule", "ReviewEvent"],
5199
5568
  // The FULL published surface (the §11 core plus the rest of M1–M8). Adding a
5200
5569
  // name here without a matching re-export (or vice-versa) breaks the drift test.
@@ -5245,13 +5614,22 @@ var CONTRACT_MANIFEST = {
5245
5614
  "ReviewerAction",
5246
5615
  "ReviewDecisionContext",
5247
5616
  "ReviewerDecisionRequest",
5248
- "ReviewerDecisionResult"
5617
+ "ReviewerDecisionResult",
5618
+ // agent commerce request plane
5619
+ "CommerceBlocker",
5620
+ "QuoteDomainRequest",
5621
+ "DomainQuote",
5622
+ "CommerceRequestKind",
5623
+ "RequestDomainPurchaseRequest",
5624
+ "RequestPlanChangeRequest",
5625
+ "ListCommerceRequestsParams",
5626
+ "CommerceRequest"
5249
5627
  ],
5250
5628
  // The complete Review Loop behavior lives in the send skill; writing-rule
5251
5629
  // governance remains independently installable and part of this contract.
5252
5630
  skills: ["extrovert-send-email", "extrovert-writing-rules"]
5253
5631
  };
5254
5632
 
5255
- export { API_VERSION_HEADER, ApiError, AuthenticationError, BornStaleError, BreadthRequiredError, CONTRACT_MANIFEST, CONTRACT_VERSION, CURRENT_API_VERSION, Categories, ConflictError, ConnectionError, ContactLists, DEFAULT_BASE_URL, Domains, ExtrovertClient as Extrovert, ExtrovertClient, ForbiddenScopeError, IdempotencyConflictError, InboxHandle, Inboxes, IntentRequiredError, ListPage, MOCK_BASE_URL, Messages, MockBackend, NotFoundError, PROBLEM_CODES, PaymentRequiredError, PermissionError, ProjectInboxes, Projects, REVIEW_PROBLEM_RETRYABLE, RateLimitError, RecipientSuppressedError, ReviewConflictError, ReviewEvents, Reviews, Rules, SDK_VERSION, SendNeedsReconciliationError, StaleError, Suppressions, TerminalError, Threads, TimeoutError, UnavailableError, ValidationError, Webhooks, WrongStateError, extractCredentials, extractLink, extractOtp, isProblemCode, isQueuedForReview, isSentImmediately, listPage, parseKeyTier, parseProblem, parseWebhook, reviewIdOf, sentMessageIdOf, serializeInclude, signWebhook, threadIdOf, tierAllowsOrgWildcard, tierNeedsExplicitBreadth, verifyWebhookSignature };
5633
+ export { API_VERSION_HEADER, ApiError, AuthenticationError, BornStaleError, BreadthRequiredError, CONTRACT_MANIFEST, CONTRACT_VERSION, CURRENT_API_VERSION, Categories, Commerce, ConflictError, ConnectionError, ContactLists, DEFAULT_BASE_URL, Domains, ExtrovertClient as Extrovert, ExtrovertClient, ForbiddenScopeError, IdempotencyConflictError, InboxHandle, Inboxes, IntentRequiredError, ListPage, MOCK_BASE_URL, Messages, MockBackend, NotFoundError, PROBLEM_CODES, PaymentRequiredError, PermissionError, ProjectInboxes, Projects, REVIEW_PROBLEM_RETRYABLE, RateLimitError, RecipientSuppressedError, ReviewConflictError, ReviewEvents, Reviews, Rules, SDK_VERSION, SendNeedsReconciliationError, StaleError, Suppressions, TerminalError, Threads, TimeoutError, UnavailableError, ValidationError, Webhooks, WrongStateError, extractCredentials, extractLink, extractOtp, isProblemCode, isQueuedForReview, isSentImmediately, listPage, parseKeyTier, parseProblem, parseWebhook, reviewIdOf, sentMessageIdOf, serializeInclude, signWebhook, threadIdOf, tierAllowsOrgWildcard, tierNeedsExplicitBreadth, verifyWebhookSignature };
5256
5634
  //# sourceMappingURL=index.js.map
5257
5635
  //# sourceMappingURL=index.js.map