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