@extrovert.dev/sdk 0.1.0-pre.6 → 0.1.0-pre.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -65
- package/dist/index.cjs +532 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +549 -278
- package/dist/index.d.ts +549 -278
- package/dist/index.js +532 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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}
|
|
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.
|
|
271
|
+
var SDK_VERSION = "0.1.0-pre.8";
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -913,6 +938,8 @@ ${parent.text}`;
|
|
|
913
938
|
var MockBackend = class {
|
|
914
939
|
constructor() {
|
|
915
940
|
this.state = freshState();
|
|
941
|
+
/** Save / edit a rule (mock) - append-only by supersession (D11). */
|
|
942
|
+
this.learnedRules = /* @__PURE__ */ new Map();
|
|
916
943
|
}
|
|
917
944
|
reset() {
|
|
918
945
|
this.state = freshState();
|
|
@@ -936,7 +963,7 @@ var MockBackend = class {
|
|
|
936
963
|
agent_id: agentId,
|
|
937
964
|
agent_key: `pk_agent_proj_${agentId.slice(4)}_${rid("sk").slice(3)}`,
|
|
938
965
|
scopes: ["mailbox:create", "mailbox:read", "mailbox:send", "webhook:write"],
|
|
939
|
-
// The
|
|
966
|
+
// The issued key is bound to the token's resolved org/project; the agent cannot change it.
|
|
940
967
|
org_id: MOCK_ORG_ID,
|
|
941
968
|
project_id: MOCK_PROJECT_ID
|
|
942
969
|
};
|
|
@@ -951,7 +978,7 @@ var MockBackend = class {
|
|
|
951
978
|
const existing = this.state.signupByEmail.get(email);
|
|
952
979
|
const customerId = existing?.customerId ?? `cus_pn_signup_${rid("c").slice(2)}`;
|
|
953
980
|
const agentId = existing?.agentId ?? rid("agt");
|
|
954
|
-
const address = existing?.address ?? `${req.username ?? randomHandle()}
|
|
981
|
+
const address = existing?.address ?? `${validatedSharedLocalPart(req.username ?? randomHandle())}@${FREE_SHARED_DOMAIN}`;
|
|
955
982
|
const otp = "492013";
|
|
956
983
|
this.state.signupByEmail.set(email, { customerId, agentId, address, otp, verified: false });
|
|
957
984
|
return {
|
|
@@ -1007,8 +1034,10 @@ var MockBackend = class {
|
|
|
1007
1034
|
}
|
|
1008
1035
|
}
|
|
1009
1036
|
const metadata = req.metadata ? mergeMetadata({}, req.metadata) : {};
|
|
1010
|
-
const
|
|
1011
|
-
const
|
|
1037
|
+
const domain = req.domain ?? PAID_SHARED_DOMAIN;
|
|
1038
|
+
const normalizedDomain = domain.trim().toLowerCase();
|
|
1039
|
+
const isSharedDomain = normalizedDomain === PAID_SHARED_DOMAIN || normalizedDomain === FREE_SHARED_DOMAIN;
|
|
1040
|
+
const username = isSharedDomain ? validatedSharedLocalPart(req.username ?? randomHandle()) : req.username ?? randomHandle();
|
|
1012
1041
|
const id = rid("ibx");
|
|
1013
1042
|
const inbox = {
|
|
1014
1043
|
object: "inbox",
|
|
@@ -1023,6 +1052,7 @@ var MockBackend = class {
|
|
|
1023
1052
|
onboarding_mode: req.domain ? "ns_delegated" : "shared",
|
|
1024
1053
|
agent_id: null,
|
|
1025
1054
|
daily_send_limit: DEFAULT_DAILY_SEND_LIMIT,
|
|
1055
|
+
direct_smtp_enabled: false,
|
|
1026
1056
|
webhook_url: req.webhook_url ?? null,
|
|
1027
1057
|
metadata,
|
|
1028
1058
|
created_at: now(),
|
|
@@ -1061,7 +1091,7 @@ var MockBackend = class {
|
|
|
1061
1091
|
* Normalize an inbox ref (opaque id OR address alias) to the canonical address the
|
|
1062
1092
|
* mock keys its message/thread/contact maps on. The SDK now routes inbox ops by the
|
|
1063
1093
|
* 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
|
|
1094
|
+
* canonical-key semantics), so the mock must resolve an id back to its address -
|
|
1065
1095
|
* both key `state.inboxes` (same object), `state.messages` keys by address only.
|
|
1066
1096
|
* Unknown refs pass through unchanged so the existing not-found paths still fire.
|
|
1067
1097
|
*/
|
|
@@ -1260,8 +1290,8 @@ var MockBackend = class {
|
|
|
1260
1290
|
}
|
|
1261
1291
|
/**
|
|
1262
1292
|
* 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
|
|
1293
|
+
* the real server, so it is literally the same call here: the resolved policy -
|
|
1294
|
+
* not which SDK method you picked - decides whether the message is queued
|
|
1265
1295
|
* (`kind:"queued_for_review"`) or delivered.
|
|
1266
1296
|
*/
|
|
1267
1297
|
submitForReview(address, req) {
|
|
@@ -1343,7 +1373,7 @@ var MockBackend = class {
|
|
|
1343
1373
|
this.state.reviews.set(review.id, review);
|
|
1344
1374
|
this.enqueueTerminalNudge(review);
|
|
1345
1375
|
}
|
|
1346
|
-
/** Raw delivery for a send
|
|
1376
|
+
/** Raw delivery for a send - no policy, only reachable from submitOutbound. */
|
|
1347
1377
|
deliverSend(address, req) {
|
|
1348
1378
|
return this.deliverRaw(address, {
|
|
1349
1379
|
to: toArray(req.to),
|
|
@@ -1355,7 +1385,7 @@ var MockBackend = class {
|
|
|
1355
1385
|
attachments: req.attachments
|
|
1356
1386
|
});
|
|
1357
1387
|
}
|
|
1358
|
-
/** Raw delivery for a reply
|
|
1388
|
+
/** Raw delivery for a reply - no policy, only reachable from submitOutbound. */
|
|
1359
1389
|
deliverReply(address, req, env) {
|
|
1360
1390
|
return this.deliverRaw(address, {
|
|
1361
1391
|
to: env.to,
|
|
@@ -1393,6 +1423,13 @@ var MockBackend = class {
|
|
|
1393
1423
|
* the server does before it writes the review row.
|
|
1394
1424
|
*/
|
|
1395
1425
|
deriveReplyEnvelope(address, req) {
|
|
1426
|
+
if (Boolean(req.thread_id) === Boolean(req.message_id)) {
|
|
1427
|
+
throw new ValidationError({
|
|
1428
|
+
status: 400,
|
|
1429
|
+
code: "bad_request",
|
|
1430
|
+
message: "provide exactly one of thread_id or message_id"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1396
1433
|
const all = this.state.messages.get(address) ?? [];
|
|
1397
1434
|
let parent;
|
|
1398
1435
|
let threadId = req.thread_id;
|
|
@@ -1410,6 +1447,13 @@ var MockBackend = class {
|
|
|
1410
1447
|
message: "thread_id or message_id is required"
|
|
1411
1448
|
});
|
|
1412
1449
|
}
|
|
1450
|
+
if (req.thread_id && req.expected_last_message_id && parent?.id !== req.expected_last_message_id) {
|
|
1451
|
+
throw new ConflictError({
|
|
1452
|
+
status: 409,
|
|
1453
|
+
code: "conflict",
|
|
1454
|
+
message: `thread advanced; latest message is ${parent?.id ?? "unknown"}`
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1413
1457
|
const to = [];
|
|
1414
1458
|
if (parent) {
|
|
1415
1459
|
to.push(parent.from.email);
|
|
@@ -1668,7 +1712,7 @@ var MockBackend = class {
|
|
|
1668
1712
|
* review id so a test can drive the reviewer decision plane offline. `createdAtMs`
|
|
1669
1713
|
* (optional) backdates created_at so a test can trip the hard review_deadline breaker.
|
|
1670
1714
|
*/
|
|
1671
|
-
seedReviewerHeldReview(opts = { fromAddress: "
|
|
1715
|
+
seedReviewerHeldReview(opts = { fromAddress: "reviewer@extrovertmail.com" }) {
|
|
1672
1716
|
const review = this.createReviewRecord(opts.fromAddress, {
|
|
1673
1717
|
kind: "send",
|
|
1674
1718
|
subject: "Pilot proposal",
|
|
@@ -1774,7 +1818,7 @@ var MockBackend = class {
|
|
|
1774
1818
|
// ---- Category registry (Review Loop, D9/D10) --------------------------
|
|
1775
1819
|
/**
|
|
1776
1820
|
* Browse the registry (mock), newest-first, excluding merged/soft-deleted. `match`
|
|
1777
|
-
* is a pure lexical filter (every token must appear in name+description)
|
|
1821
|
+
* is a pure lexical filter (every token must appear in name+description) - NO LLM,
|
|
1778
1822
|
* mirroring the server.
|
|
1779
1823
|
*/
|
|
1780
1824
|
listCategories(params = {}) {
|
|
@@ -1815,7 +1859,7 @@ var MockBackend = class {
|
|
|
1815
1859
|
this.state.categories.set(cat.id, cat);
|
|
1816
1860
|
return cat;
|
|
1817
1861
|
}
|
|
1818
|
-
/** Rename / re-describe a category (mock)
|
|
1862
|
+
/** Rename / re-describe a category (mock) - metadata only (D10). */
|
|
1819
1863
|
updateCategory(categoryId, req) {
|
|
1820
1864
|
const cat = this.state.categories.get(categoryId);
|
|
1821
1865
|
if (!cat) return void 0;
|
|
@@ -1825,7 +1869,7 @@ var MockBackend = class {
|
|
|
1825
1869
|
this.state.categories.set(cat.id, cat);
|
|
1826
1870
|
return cat;
|
|
1827
1871
|
}
|
|
1828
|
-
// ---- Graduation + risk dial (Review Loop, D16/D6/D17)
|
|
1872
|
+
// ---- Graduation + risk dial (Review Loop, D16/D6/D17) - agent READ + PROPOSE --
|
|
1829
1873
|
/** The mock account-default risk dial (mirrors the server defaults). */
|
|
1830
1874
|
accountDial() {
|
|
1831
1875
|
return {
|
|
@@ -1841,7 +1885,7 @@ var MockBackend = class {
|
|
|
1841
1885
|
/**
|
|
1842
1886
|
* Read the effective risk dial (mock): the account default + every category with an
|
|
1843
1887
|
* inherited (null override) effective dial. The mock category carries no overrides,
|
|
1844
|
-
* so every category inherits
|
|
1888
|
+
* so every category inherits - effective == account.
|
|
1845
1889
|
*/
|
|
1846
1890
|
getRiskDial() {
|
|
1847
1891
|
const account = this.accountDial();
|
|
@@ -1895,7 +1939,7 @@ var MockBackend = class {
|
|
|
1895
1939
|
}
|
|
1896
1940
|
/**
|
|
1897
1941
|
* Propose graduating a category (mock): returns the current gate status without
|
|
1898
|
-
* changing the category state (D16
|
|
1942
|
+
* changing the category state (D16 - an agent can never flip the bit).
|
|
1899
1943
|
*/
|
|
1900
1944
|
proposeGraduation(categoryId, _req) {
|
|
1901
1945
|
return this.getGraduationStatus(categoryId);
|
|
@@ -1904,7 +1948,7 @@ var MockBackend = class {
|
|
|
1904
1948
|
* Read the D19/§8 backlog-reconciliation status (mock): counts the QUEUED drafts in a
|
|
1905
1949
|
* category that are stale vs current-enough against the current rules-version. The
|
|
1906
1950
|
* 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)
|
|
1951
|
+
* draft reads as current-enough (composed 0 vs current 0) - the contract shape is
|
|
1908
1952
|
* exercised; the integer-compare logic is covered by the Go tests.
|
|
1909
1953
|
*/
|
|
1910
1954
|
getScanBacklogStatus(categoryId) {
|
|
@@ -1928,7 +1972,7 @@ var MockBackend = class {
|
|
|
1928
1972
|
};
|
|
1929
1973
|
}
|
|
1930
1974
|
/**
|
|
1931
|
-
* Read the demand-driven pacing state (mock
|
|
1975
|
+
* Read the demand-driven pacing state (mock - M7 Slice B/§8): the cursor + effective
|
|
1932
1976
|
* window/ceiling/interval + each queued draft's classification. The mock has no cursor
|
|
1933
1977
|
* (nothing reviewed) and no composed_* stamps, so every queued draft reads in-window-
|
|
1934
1978
|
* fresh until the window fills, then ahead; the contract shape is exercised (the
|
|
@@ -1968,7 +2012,7 @@ var MockBackend = class {
|
|
|
1968
2012
|
const human = r.author_kind === "human" ? 1 : 0;
|
|
1969
2013
|
return [hard, spec, human, r.rev, r.priority];
|
|
1970
2014
|
}
|
|
1971
|
-
/** Get the ORDERED active rule set (mock)
|
|
2015
|
+
/** Get the ORDERED active rule set (mock) - §7 ladder + category-before-general. */
|
|
1972
2016
|
getRules(params = {}) {
|
|
1973
2017
|
const active = [...this.state.rules.values()].filter((r) => r.status === "active");
|
|
1974
2018
|
const byRank = (a, b) => {
|
|
@@ -2000,7 +2044,27 @@ var MockBackend = class {
|
|
|
2000
2044
|
composition_token_expires_at: params.scope ? void 0 : new Date(Date.now() + 6e5).toISOString()
|
|
2001
2045
|
};
|
|
2002
2046
|
}
|
|
2003
|
-
|
|
2047
|
+
learnReviewRule(reviewId, req) {
|
|
2048
|
+
const fingerprint = JSON.stringify({ reviewId, req });
|
|
2049
|
+
const prior = this.learnedRules.get(req.client_id);
|
|
2050
|
+
if (prior) {
|
|
2051
|
+
if (prior.fingerprint !== fingerprint) throw new ValidationError({ status: 409, code: "conflict", message: "learning retry identity changed" });
|
|
2052
|
+
return structuredClone(prior.result);
|
|
2053
|
+
}
|
|
2054
|
+
const turn = (this.state.reviewTurns.get(reviewId) ?? []).find((t) => t.id === req.source_turn_id);
|
|
2055
|
+
if (!turn || turn.actor_kind !== "human" || !turn.actor_id) throw new ValidationError({ status: 403, code: "forbidden_scope", message: "learning requires authenticated human feedback" });
|
|
2056
|
+
const rule = this.saveRule({ ...req, scope: req.target === "category" ? "category" : "general", source_review_id: reviewId, source_turn_id: turn.id });
|
|
2057
|
+
rule.source_turn_id = turn.id;
|
|
2058
|
+
rule.rule_layer = req.target === "org_house" ? "org" : "project";
|
|
2059
|
+
if (req.target === "org_house") delete rule.project_id;
|
|
2060
|
+
rule.source_review_id = reviewId;
|
|
2061
|
+
rule.source_turn_id = turn.id;
|
|
2062
|
+
const audit = [...this.state.ruleAudit.values()].find((entry) => entry.entity_id === rule.id);
|
|
2063
|
+
audit.after_json = ruleSnapshotJSON(rule);
|
|
2064
|
+
const result = { rule, source_review_id: reviewId, source_turn_id: turn.id, human_id: turn.actor_id, audit_id: audit.id, propagation: "queued" };
|
|
2065
|
+
this.learnedRules.set(req.client_id, { fingerprint, result: structuredClone(result) });
|
|
2066
|
+
return result;
|
|
2067
|
+
}
|
|
2004
2068
|
saveRule(req) {
|
|
2005
2069
|
const text = req.rule_text.trim();
|
|
2006
2070
|
if (!text) {
|
|
@@ -2089,7 +2153,7 @@ var MockBackend = class {
|
|
|
2089
2153
|
this.recordRuleAudit("supersede", next.id, ruleSnapshotJSON(prior), ruleSnapshotJSON(next));
|
|
2090
2154
|
return next;
|
|
2091
2155
|
}
|
|
2092
|
-
/** Retire a rule (mock)
|
|
2156
|
+
/** Retire a rule (mock) - soft delete, or undefined when unknown. */
|
|
2093
2157
|
retireRule(ruleId) {
|
|
2094
2158
|
const rule = this.state.rules.get(ruleId);
|
|
2095
2159
|
if (!rule) return void 0;
|
|
@@ -2107,7 +2171,7 @@ var MockBackend = class {
|
|
|
2107
2171
|
if (params.entity_id) items = items.filter((e) => e.entity_id === params.entity_id);
|
|
2108
2172
|
return { items, total: items.length };
|
|
2109
2173
|
}
|
|
2110
|
-
/** Undo a rule change (mock)
|
|
2174
|
+
/** Undo a rule change (mock) - restore the prior version; idempotent (re-undo 409). */
|
|
2111
2175
|
undoRuleChange(udoId) {
|
|
2112
2176
|
const entry = this.state.ruleAudit.get(udoId);
|
|
2113
2177
|
if (!entry) throw new NotFoundError({ status: 404, code: "not_found", message: "audit row not found" });
|
|
@@ -2211,7 +2275,7 @@ var MockBackend = class {
|
|
|
2211
2275
|
}
|
|
2212
2276
|
}
|
|
2213
2277
|
/**
|
|
2214
|
-
* Enqueue `front_run_next`
|
|
2278
|
+
* Enqueue `front_run_next` - the signal that the review reached a terminal state
|
|
2215
2279
|
* while the agent was still trying to act on it.
|
|
2216
2280
|
*
|
|
2217
2281
|
* Deduped on (review, terminal state, parent revision) so a retry loop hitting
|
|
@@ -2252,8 +2316,8 @@ var MockBackend = class {
|
|
|
2252
2316
|
}
|
|
2253
2317
|
/**
|
|
2254
2318
|
* 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
|
|
2256
|
-
* console showed the error and the agent's queue stayed silent
|
|
2319
|
+
* This is the case the composing agent was previously never told about - the
|
|
2320
|
+
* console showed the error and the agent's queue stayed silent - so the loop test
|
|
2257
2321
|
* that matters most drives this path.
|
|
2258
2322
|
*/
|
|
2259
2323
|
simulateSendFailed(reviewId, error = "provider rejected the message") {
|
|
@@ -2338,7 +2402,7 @@ var MockBackend = class {
|
|
|
2338
2402
|
}
|
|
2339
2403
|
/**
|
|
2340
2404
|
* 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)
|
|
2405
|
+
* returns the immediate drain (empty when caught up) - the server's "empty on
|
|
2342
2406
|
* timeout" contract.
|
|
2343
2407
|
*/
|
|
2344
2408
|
waitForReviewEvent(params = {}) {
|
|
@@ -2427,7 +2491,7 @@ var MockBackend = class {
|
|
|
2427
2491
|
html: opts.html ?? null,
|
|
2428
2492
|
extracted_text: opts.text.trim() || null,
|
|
2429
2493
|
extracted_html: opts.html?.trim() || null,
|
|
2430
|
-
message_id: `<${id}@${address.split("@")[1] ??
|
|
2494
|
+
message_id: `<${id}@${address.split("@")[1] ?? PAID_SHARED_DOMAIN}>`,
|
|
2431
2495
|
folder: opts.direction === "inbound" ? "INBOX" : "Sent",
|
|
2432
2496
|
seen: opts.direction === "outbound",
|
|
2433
2497
|
date: now()
|
|
@@ -2543,10 +2607,10 @@ var MockBackend = class {
|
|
|
2543
2607
|
if (offset + page.length < total) result.next_cursor = String(offset + page.length);
|
|
2544
2608
|
return result;
|
|
2545
2609
|
}
|
|
2546
|
-
listThreads(address) {
|
|
2610
|
+
listThreads(address, params = {}) {
|
|
2547
2611
|
address = this.addrOf(address);
|
|
2548
2612
|
const items = this.threadsFor(address);
|
|
2549
|
-
return
|
|
2613
|
+
return this.paginateThreads(items, params);
|
|
2550
2614
|
}
|
|
2551
2615
|
/** Thread-level search (subject / snippet / participant substring). */
|
|
2552
2616
|
searchThreads(address, params) {
|
|
@@ -2555,7 +2619,17 @@ var MockBackend = class {
|
|
|
2555
2619
|
const items = this.threadsFor(address).filter(
|
|
2556
2620
|
(t) => t.subject.toLowerCase().includes(q) || t.snippet.toLowerCase().includes(q) || t.participants.join(" ").toLowerCase().includes(q)
|
|
2557
2621
|
);
|
|
2558
|
-
return
|
|
2622
|
+
return this.paginateThreads(items, params);
|
|
2623
|
+
}
|
|
2624
|
+
paginateThreads(items, params) {
|
|
2625
|
+
const total = items.length;
|
|
2626
|
+
const cursorOffset = params.cursor === void 0 ? void 0 : Number(params.cursor);
|
|
2627
|
+
const offset = params.offset ?? (Number.isFinite(cursorOffset) ? cursorOffset : 0);
|
|
2628
|
+
const limit = params.limit ?? 25;
|
|
2629
|
+
const page = items.slice(offset, offset + limit);
|
|
2630
|
+
const result = { items: page, total };
|
|
2631
|
+
if (offset + page.length < total) result.next_cursor = String(offset + page.length);
|
|
2632
|
+
return result;
|
|
2559
2633
|
}
|
|
2560
2634
|
/** Fetch one thread (with messages, oldest-first) by id under an inbox. */
|
|
2561
2635
|
getThread(address, threadId) {
|
|
@@ -2627,17 +2701,18 @@ var MockBackend = class {
|
|
|
2627
2701
|
arr.push(m);
|
|
2628
2702
|
byThread.set(m.thread_id, arr);
|
|
2629
2703
|
}
|
|
2630
|
-
const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms));
|
|
2704
|
+
const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms, true));
|
|
2631
2705
|
items.sort((a, b) => b.last_message_at.localeCompare(a.last_message_at));
|
|
2632
2706
|
return items;
|
|
2633
2707
|
}
|
|
2634
|
-
buildThread(address, id, ms) {
|
|
2708
|
+
buildThread(address, id, ms, summary = false) {
|
|
2635
2709
|
const sorted = [...ms].sort((a, b) => a.date.localeCompare(b.date));
|
|
2636
2710
|
const last = sorted[sorted.length - 1];
|
|
2637
2711
|
const seen = /* @__PURE__ */ new Set();
|
|
2638
2712
|
const participants = [];
|
|
2639
|
-
|
|
2640
|
-
|
|
2713
|
+
const participantMessages = summary ? [last] : sorted;
|
|
2714
|
+
for (const m of participantMessages) {
|
|
2715
|
+
for (const a of [m.from, ...m.to, ...m.cc ?? [], ...m.reply_to ?? []]) {
|
|
2641
2716
|
const key = a.email.toLowerCase();
|
|
2642
2717
|
if (seen.has(key)) continue;
|
|
2643
2718
|
seen.add(key);
|
|
@@ -2651,7 +2726,10 @@ var MockBackend = class {
|
|
|
2651
2726
|
participants,
|
|
2652
2727
|
message_count: sorted.length,
|
|
2653
2728
|
last_message_at: last.date,
|
|
2654
|
-
snippet: (last.text ?? last.html ?? "").slice(0, 120)
|
|
2729
|
+
snippet: (last.text ?? last.html ?? "").slice(0, 120),
|
|
2730
|
+
unread: !last.seen,
|
|
2731
|
+
last_message_has_attachments: (this.state.attachments.get(last.id)?.length ?? 0) > 0,
|
|
2732
|
+
last_message_id: last.id
|
|
2655
2733
|
};
|
|
2656
2734
|
}
|
|
2657
2735
|
/**
|
|
@@ -2811,24 +2889,34 @@ ${text}`)) {
|
|
|
2811
2889
|
return this.state.contactLists.delete(entryId);
|
|
2812
2890
|
}
|
|
2813
2891
|
// ---- domains (Slice 5) ------------------------------------------------
|
|
2814
|
-
/**
|
|
2892
|
+
/** Add a delegated domain and return only the customer-published nameservers. */
|
|
2815
2893
|
onboardDomain(req) {
|
|
2816
2894
|
assertProjectMatch(req.project_id);
|
|
2817
2895
|
const name = req.domain.trim().toLowerCase();
|
|
2818
2896
|
const existing = this.state.domains.get(name);
|
|
2819
2897
|
if (existing) return { ...existing };
|
|
2820
|
-
const mode =
|
|
2898
|
+
const mode = "ns_delegated";
|
|
2821
2899
|
const domain = {
|
|
2822
2900
|
id: rid("dom"),
|
|
2823
2901
|
domain: name,
|
|
2824
2902
|
mode,
|
|
2825
|
-
verification_status:
|
|
2826
|
-
dkim_status:
|
|
2827
|
-
shared:
|
|
2903
|
+
verification_status: "verifying",
|
|
2904
|
+
dkim_status: "configured",
|
|
2905
|
+
shared: false,
|
|
2828
2906
|
created_at: now(),
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2907
|
+
delegation_ns: domainDelegationNS(name),
|
|
2908
|
+
instruction: "Add the nameserver entries at your domain provider. We check automatically; use Recheck DNS for an immediate check.",
|
|
2909
|
+
readiness: {
|
|
2910
|
+
status: "waiting_for_dns",
|
|
2911
|
+
label: "Waiting for DNS",
|
|
2912
|
+
summary: "We have not confirmed your nameserver entries yet. Add them at your domain provider; we will finish setup automatically.",
|
|
2913
|
+
reason: "dns_entries_unconfirmed",
|
|
2914
|
+
action_required_by: "customer",
|
|
2915
|
+
next_action: "check_dns_entries",
|
|
2916
|
+
ready_for_inboxes: false,
|
|
2917
|
+
poll_after_seconds: 30,
|
|
2918
|
+
inboxes: { scope: "agent", total: 0, ready: 0, setting_up: 0, needs_attention: 0 }
|
|
2919
|
+
}
|
|
2832
2920
|
};
|
|
2833
2921
|
this.state.domains.set(name, domain);
|
|
2834
2922
|
return { ...domain };
|
|
@@ -2876,6 +2964,129 @@ ${text}`)) {
|
|
|
2876
2964
|
const job = this.state.jobs.get(jobId);
|
|
2877
2965
|
return job ? { ...job } : void 0;
|
|
2878
2966
|
}
|
|
2967
|
+
// ---- commerce (quote/request/status; no human approval mutation) ------
|
|
2968
|
+
quoteDomain(req) {
|
|
2969
|
+
const domain = req.domain.trim().toLowerCase();
|
|
2970
|
+
return {
|
|
2971
|
+
object: "domain_quote",
|
|
2972
|
+
domain,
|
|
2973
|
+
available: !domain.startsWith("unavailable."),
|
|
2974
|
+
currency: "usd",
|
|
2975
|
+
quote_cents: 2500,
|
|
2976
|
+
renewal_cents: 2500,
|
|
2977
|
+
premium: false,
|
|
2978
|
+
quote_expires_at: new Date(Date.now() + 15 * 60 * 1e3).toISOString(),
|
|
2979
|
+
blockers: []
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
requestDomainPurchase(req) {
|
|
2983
|
+
const idem = `domain_purchase:${req.idempotency_key.trim()}`;
|
|
2984
|
+
const replayId = this.state.commerceByIdempotency.get(idem);
|
|
2985
|
+
if (replayId) return { ...this.state.commerceRequests.get(replayId) };
|
|
2986
|
+
const quote = this.quoteDomain({ domain: req.domain });
|
|
2987
|
+
const timestamp = now();
|
|
2988
|
+
const id = rid("creq");
|
|
2989
|
+
const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
|
|
2990
|
+
const request = {
|
|
2991
|
+
object: "commerce_request",
|
|
2992
|
+
id,
|
|
2993
|
+
kind: "domain_purchase",
|
|
2994
|
+
state: "awaiting_human_approval",
|
|
2995
|
+
domain: quote.domain,
|
|
2996
|
+
domain_scope: req.scope ?? "org",
|
|
2997
|
+
rationale: req.rationale,
|
|
2998
|
+
currency: quote.currency,
|
|
2999
|
+
quote_cents: quote.quote_cents,
|
|
3000
|
+
renewal_cents: quote.renewal_cents,
|
|
3001
|
+
quote_expires_at: quote.quote_expires_at,
|
|
3002
|
+
auto_renew: req.auto_renew ?? true,
|
|
3003
|
+
blocker_code: "human_approval_required",
|
|
3004
|
+
blockers: [
|
|
3005
|
+
{
|
|
3006
|
+
code: "human_approval_required",
|
|
3007
|
+
message: "A human billing administrator must approve this domain purchase.",
|
|
3008
|
+
manage_url: approvalUrl
|
|
3009
|
+
}
|
|
3010
|
+
],
|
|
3011
|
+
approval_url: approvalUrl,
|
|
3012
|
+
agent_next_action: "Share approval_url with the human, then poll this request after approval.",
|
|
3013
|
+
retry_safe: true,
|
|
3014
|
+
poll_after_seconds: 10,
|
|
3015
|
+
version: 1,
|
|
3016
|
+
created_at: timestamp,
|
|
3017
|
+
updated_at: timestamp
|
|
3018
|
+
};
|
|
3019
|
+
this.state.commerceRequests.set(id, request);
|
|
3020
|
+
this.state.commerceByIdempotency.set(idem, id);
|
|
3021
|
+
return { ...request };
|
|
3022
|
+
}
|
|
3023
|
+
requestPlanChange(req) {
|
|
3024
|
+
const idem = `plan_change:${req.idempotency_key.trim()}`;
|
|
3025
|
+
const replayId = this.state.commerceByIdempotency.get(idem);
|
|
3026
|
+
if (replayId) return { ...this.state.commerceRequests.get(replayId) };
|
|
3027
|
+
const timestamp = now();
|
|
3028
|
+
const id = rid("creq");
|
|
3029
|
+
const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
|
|
3030
|
+
const request = {
|
|
3031
|
+
object: "commerce_request",
|
|
3032
|
+
id,
|
|
3033
|
+
kind: "plan_change",
|
|
3034
|
+
state: "awaiting_human_approval",
|
|
3035
|
+
target_plan: req.target_plan,
|
|
3036
|
+
current_plan: "developer",
|
|
3037
|
+
rationale: req.rationale,
|
|
3038
|
+
currency: "usd",
|
|
3039
|
+
quote_cents: 0,
|
|
3040
|
+
renewal_cents: 0,
|
|
3041
|
+
auto_renew: true,
|
|
3042
|
+
blocker_code: "human_approval_required",
|
|
3043
|
+
blockers: [
|
|
3044
|
+
{
|
|
3045
|
+
code: "human_approval_required",
|
|
3046
|
+
message: "A human billing administrator must approve this plan change.",
|
|
3047
|
+
manage_url: approvalUrl
|
|
3048
|
+
}
|
|
3049
|
+
],
|
|
3050
|
+
approval_url: approvalUrl,
|
|
3051
|
+
agent_next_action: "Share approval_url with the human, then poll this request after approval.",
|
|
3052
|
+
retry_safe: true,
|
|
3053
|
+
poll_after_seconds: 10,
|
|
3054
|
+
version: 1,
|
|
3055
|
+
created_at: timestamp,
|
|
3056
|
+
updated_at: timestamp
|
|
3057
|
+
};
|
|
3058
|
+
this.state.commerceRequests.set(id, request);
|
|
3059
|
+
this.state.commerceByIdempotency.set(idem, id);
|
|
3060
|
+
return { ...request };
|
|
3061
|
+
}
|
|
3062
|
+
getCommerceRequest(requestId) {
|
|
3063
|
+
const request = this.state.commerceRequests.get(requestId);
|
|
3064
|
+
return request ? { ...request } : void 0;
|
|
3065
|
+
}
|
|
3066
|
+
cancelCommerceRequest(requestId) {
|
|
3067
|
+
const request = this.state.commerceRequests.get(requestId);
|
|
3068
|
+
if (!request) return void 0;
|
|
3069
|
+
if (!["awaiting_human_approval", "blocked", "approved", "payment_action_required", "payment_failed"].includes(request.state)) {
|
|
3070
|
+
throw new ValidationError({ status: 409, code: "conflict", message: "this request can no longer be cancelled" });
|
|
3071
|
+
}
|
|
3072
|
+
request.state = "cancelled";
|
|
3073
|
+
request.blocker_code = void 0;
|
|
3074
|
+
request.blockers = [];
|
|
3075
|
+
request.agent_next_action = "The request is cancelled. Create a new request only if the purchase is still needed.";
|
|
3076
|
+
request.version += 1;
|
|
3077
|
+
request.updated_at = now();
|
|
3078
|
+
return { ...request };
|
|
3079
|
+
}
|
|
3080
|
+
listCommerceRequests(params = {}) {
|
|
3081
|
+
let items = [...this.state.commerceRequests.values()];
|
|
3082
|
+
items.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
3083
|
+
const total = items.length;
|
|
3084
|
+
const offset = params.page ? Math.max(0, Number.parseInt(params.page, 10) || 0) : 0;
|
|
3085
|
+
const limit = Math.max(1, Math.min(params.limit ?? 50, 100));
|
|
3086
|
+
const page = items.slice(offset, offset + limit).map((request) => ({ ...request }));
|
|
3087
|
+
const next = offset + page.length;
|
|
3088
|
+
return { items: page, total, next_cursor: next < total ? String(next) : void 0 };
|
|
3089
|
+
}
|
|
2879
3090
|
// ---- suppressions (recipient opt-outs / list-unsubscribe) --------------
|
|
2880
3091
|
/**
|
|
2881
3092
|
* Pre-check whether the caller's org suppresses a recipient (mirrors
|
|
@@ -2919,7 +3130,7 @@ ${text}`)) {
|
|
|
2919
3130
|
/**
|
|
2920
3131
|
* Reject the WHOLE send if ANY recipient has an active org-scope suppression,
|
|
2921
3132
|
* naming exactly the suppressed addresses (never the scope/origin) so the caller
|
|
2922
|
-
* can drop them and retry
|
|
3133
|
+
* can drop them and retry - mirroring the live `recipient_suppressed` (422) path.
|
|
2923
3134
|
*/
|
|
2924
3135
|
enforceSuppression(recipients) {
|
|
2925
3136
|
const active = new Set(
|
|
@@ -3002,16 +3213,6 @@ function redactSecret(w) {
|
|
|
3002
3213
|
const { secret: _omit, ...rest } = w;
|
|
3003
3214
|
return rest;
|
|
3004
3215
|
}
|
|
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
3216
|
function domainDelegationNS(domain) {
|
|
3016
3217
|
return [
|
|
3017
3218
|
{ name: domain, type: "NS", value: "ns1.extrovert.dev", ttl: 300 },
|
|
@@ -3395,8 +3596,11 @@ var HttpTransport = class {
|
|
|
3395
3596
|
signal
|
|
3396
3597
|
});
|
|
3397
3598
|
}
|
|
3398
|
-
listDomains(signal) {
|
|
3399
|
-
return this.call({ method: "GET", path: "/v1/domains", signal });
|
|
3599
|
+
listDomains(signal, params = {}) {
|
|
3600
|
+
return this.call({ method: "GET", path: "/v1/domains", query: params, signal });
|
|
3601
|
+
}
|
|
3602
|
+
listDomainEvents(domain, params, signal) {
|
|
3603
|
+
return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}/events`, query: params, signal });
|
|
3400
3604
|
}
|
|
3401
3605
|
getDomain(domain, signal) {
|
|
3402
3606
|
return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}`, signal });
|
|
@@ -3404,6 +3608,8 @@ var HttpTransport = class {
|
|
|
3404
3608
|
onboardDomain(req, signal) {
|
|
3405
3609
|
return this.call({ method: "POST", path: "/v1/domains", body: req, signal });
|
|
3406
3610
|
}
|
|
3611
|
+
// Delegated domains perform an immediate authoritative DNS check. Inspect
|
|
3612
|
+
// delegation.status separately from mail readiness; 429 requests may be retried.
|
|
3407
3613
|
verifyDomain(domain, signal) {
|
|
3408
3614
|
return this.call({
|
|
3409
3615
|
method: "POST",
|
|
@@ -3428,6 +3634,54 @@ var HttpTransport = class {
|
|
|
3428
3634
|
getJob(jobId, signal) {
|
|
3429
3635
|
return this.call({ method: "GET", path: `/v1/jobs/${encodeURIComponent(jobId)}`, signal });
|
|
3430
3636
|
}
|
|
3637
|
+
quoteDomain(req, signal) {
|
|
3638
|
+
return this.call({ method: "POST", path: "/v1/commerce/domain-quotes", body: req, signal });
|
|
3639
|
+
}
|
|
3640
|
+
requestDomainPurchase(req, signal) {
|
|
3641
|
+
const { idempotency_key, ...body } = req;
|
|
3642
|
+
return this.call({
|
|
3643
|
+
method: "POST",
|
|
3644
|
+
path: "/v1/commerce/requests/domain-purchases",
|
|
3645
|
+
body,
|
|
3646
|
+
idempotencyKey: idempotency_key,
|
|
3647
|
+
signal
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
requestPlanChange(req, signal) {
|
|
3651
|
+
const { idempotency_key, ...body } = req;
|
|
3652
|
+
return this.call({
|
|
3653
|
+
method: "POST",
|
|
3654
|
+
path: "/v1/commerce/requests/plan-changes",
|
|
3655
|
+
body,
|
|
3656
|
+
idempotencyKey: idempotency_key,
|
|
3657
|
+
signal
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
getCommerceRequest(requestId, signal) {
|
|
3661
|
+
return this.call({
|
|
3662
|
+
method: "GET",
|
|
3663
|
+
path: `/v1/commerce/requests/${encodeURIComponent(requestId)}`,
|
|
3664
|
+
signal
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
cancelCommerceRequest(requestId, signal) {
|
|
3668
|
+
return this.call({
|
|
3669
|
+
method: "POST",
|
|
3670
|
+
path: `/v1/commerce/requests/${encodeURIComponent(requestId)}/cancel`,
|
|
3671
|
+
signal
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
listCommerceRequests(params, signal) {
|
|
3675
|
+
return this.call({
|
|
3676
|
+
method: "GET",
|
|
3677
|
+
path: "/v1/commerce/requests",
|
|
3678
|
+
query: {
|
|
3679
|
+
limit: params.limit,
|
|
3680
|
+
page: params.page
|
|
3681
|
+
},
|
|
3682
|
+
signal
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3431
3685
|
submitForReview(address, req, signal) {
|
|
3432
3686
|
return this.call({
|
|
3433
3687
|
method: "POST",
|
|
@@ -3451,6 +3705,7 @@ var HttpTransport = class {
|
|
|
3451
3705
|
state: Array.isArray(params.state) ? params.state.join(",") : params.state,
|
|
3452
3706
|
category_id: params.category_id,
|
|
3453
3707
|
inbox: params.inbox,
|
|
3708
|
+
composer: params.composer,
|
|
3454
3709
|
limit: params.limit,
|
|
3455
3710
|
page: params.page
|
|
3456
3711
|
};
|
|
@@ -3533,10 +3788,12 @@ var HttpTransport = class {
|
|
|
3533
3788
|
});
|
|
3534
3789
|
}
|
|
3535
3790
|
waitForReviewEvent(params, signal) {
|
|
3791
|
+
const waitSeconds = Math.min(55, Math.max(1, params.wait_seconds ?? 55));
|
|
3536
3792
|
return this.call({
|
|
3537
3793
|
method: "GET",
|
|
3538
3794
|
path: "/v1/reviews/events/wait",
|
|
3539
|
-
query: { review_id: params.review_id, limit: params.limit, wait_seconds:
|
|
3795
|
+
query: { review_id: params.review_id, limit: params.limit, wait_seconds: waitSeconds },
|
|
3796
|
+
timeoutMs: (waitSeconds + 10) * 1e3,
|
|
3540
3797
|
signal
|
|
3541
3798
|
});
|
|
3542
3799
|
}
|
|
@@ -3605,6 +3862,9 @@ var HttpTransport = class {
|
|
|
3605
3862
|
signal
|
|
3606
3863
|
});
|
|
3607
3864
|
}
|
|
3865
|
+
learnReviewRule(reviewId, req, signal) {
|
|
3866
|
+
return this.call({ method: "POST", path: `/v1/reviews/${encodeURIComponent(reviewId)}/learned-rules`, body: req, signal });
|
|
3867
|
+
}
|
|
3608
3868
|
saveRule(req, signal) {
|
|
3609
3869
|
return this.call({
|
|
3610
3870
|
method: "PUT",
|
|
@@ -3786,8 +4046,8 @@ var MockTransport = class {
|
|
|
3786
4046
|
async searchMessages(address, params) {
|
|
3787
4047
|
return this.backend.searchMessages(address, params);
|
|
3788
4048
|
}
|
|
3789
|
-
async listThreads(address,
|
|
3790
|
-
return this.backend.listThreads(address);
|
|
4049
|
+
async listThreads(address, params) {
|
|
4050
|
+
return this.backend.listThreads(address, params);
|
|
3791
4051
|
}
|
|
3792
4052
|
async searchThreads(address, params) {
|
|
3793
4053
|
return this.backend.searchThreads(address, params);
|
|
@@ -3851,8 +4111,16 @@ var MockTransport = class {
|
|
|
3851
4111
|
if (!row) throw notFound("suppression", id);
|
|
3852
4112
|
return row;
|
|
3853
4113
|
}
|
|
3854
|
-
async listDomains() {
|
|
3855
|
-
|
|
4114
|
+
async listDomains(_signal, params = {}) {
|
|
4115
|
+
const page = this.backend.listDomains();
|
|
4116
|
+
const offset = Number(params.page ?? 0);
|
|
4117
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid page cursor");
|
|
4118
|
+
const items = page.items.slice(offset, offset + (params.limit ?? 50));
|
|
4119
|
+
return { items, total: page.total, next_cursor: offset + items.length < page.items.length ? String(offset + items.length) : void 0 };
|
|
4120
|
+
}
|
|
4121
|
+
async listDomainEvents(domain, params) {
|
|
4122
|
+
if (!this.backend.getDomain(domain)) throw notFound("domain", domain);
|
|
4123
|
+
return { items: [], next_cursor: params.after ?? "0", has_more: false, poll_after_seconds: 30 };
|
|
3856
4124
|
}
|
|
3857
4125
|
async getDomain(domain) {
|
|
3858
4126
|
const d = this.backend.getDomain(domain);
|
|
@@ -3877,6 +4145,28 @@ var MockTransport = class {
|
|
|
3877
4145
|
if (!job) throw notFound("job", jobId);
|
|
3878
4146
|
return job;
|
|
3879
4147
|
}
|
|
4148
|
+
async quoteDomain(req) {
|
|
4149
|
+
return this.backend.quoteDomain(req);
|
|
4150
|
+
}
|
|
4151
|
+
async requestDomainPurchase(req) {
|
|
4152
|
+
return this.backend.requestDomainPurchase(req);
|
|
4153
|
+
}
|
|
4154
|
+
async requestPlanChange(req) {
|
|
4155
|
+
return this.backend.requestPlanChange(req);
|
|
4156
|
+
}
|
|
4157
|
+
async getCommerceRequest(requestId) {
|
|
4158
|
+
const request = this.backend.getCommerceRequest(requestId);
|
|
4159
|
+
if (!request) throw notFound("commerce request", requestId);
|
|
4160
|
+
return request;
|
|
4161
|
+
}
|
|
4162
|
+
async cancelCommerceRequest(requestId) {
|
|
4163
|
+
const request = this.backend.cancelCommerceRequest(requestId);
|
|
4164
|
+
if (!request) throw notFound("commerce request", requestId);
|
|
4165
|
+
return request;
|
|
4166
|
+
}
|
|
4167
|
+
async listCommerceRequests(params) {
|
|
4168
|
+
return this.backend.listCommerceRequests(params);
|
|
4169
|
+
}
|
|
3880
4170
|
async submitForReview(address, req) {
|
|
3881
4171
|
return this.backend.submitForReview(address, req);
|
|
3882
4172
|
}
|
|
@@ -3982,6 +4272,9 @@ var MockTransport = class {
|
|
|
3982
4272
|
async getRules(params) {
|
|
3983
4273
|
return this.backend.getRules(params);
|
|
3984
4274
|
}
|
|
4275
|
+
async learnReviewRule(reviewId, req) {
|
|
4276
|
+
return this.backend.learnReviewRule(reviewId, req);
|
|
4277
|
+
}
|
|
3985
4278
|
async saveRule(req) {
|
|
3986
4279
|
return this.backend.saveRule(req);
|
|
3987
4280
|
}
|
|
@@ -4031,6 +4324,49 @@ function filenameFromDisposition(disposition) {
|
|
|
4031
4324
|
return m ? decodeURIComponent(m[1].trim()) : "";
|
|
4032
4325
|
}
|
|
4033
4326
|
|
|
4327
|
+
// src/domain-wait.ts
|
|
4328
|
+
async function waitForDomain(get, options = {}) {
|
|
4329
|
+
const seconds = options.timeout_seconds ?? 45;
|
|
4330
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 50) throw new Error("timeout_seconds must be an integer between 0 and 50");
|
|
4331
|
+
const deadline = AbortSignal.timeout(Math.max(1, seconds * 1e3));
|
|
4332
|
+
const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
4333
|
+
let latest;
|
|
4334
|
+
const result = (outcome) => ({
|
|
4335
|
+
domain: latest,
|
|
4336
|
+
outcome,
|
|
4337
|
+
resume_after_seconds: outcome === "timed_out" ? Math.max(5, Math.min(60, latest?.readiness?.poll_after_seconds ?? 30)) : 0
|
|
4338
|
+
});
|
|
4339
|
+
try {
|
|
4340
|
+
const singleCheckDeadline = seconds === 0 ? AbortSignal.timeout(1e4) : void 0;
|
|
4341
|
+
latest = await get(singleCheckDeadline ? options.signal ? AbortSignal.any([options.signal, singleCheckDeadline]) : singleCheckDeadline : signal);
|
|
4342
|
+
for (; ; ) {
|
|
4343
|
+
const r = latest.readiness;
|
|
4344
|
+
if (!r) return result("status_unavailable");
|
|
4345
|
+
if (r.ready_for_inboxes) return result("ready");
|
|
4346
|
+
if (r.action_required_by === "customer") return result("action_required");
|
|
4347
|
+
if (r.status === "needs_attention") return result("needs_attention");
|
|
4348
|
+
if (seconds === 0) return result("timed_out");
|
|
4349
|
+
await new Promise((resolve, reject) => {
|
|
4350
|
+
signal.throwIfAborted();
|
|
4351
|
+
const abort = () => {
|
|
4352
|
+
clearTimeout(timer);
|
|
4353
|
+
reject(signal.reason);
|
|
4354
|
+
};
|
|
4355
|
+
const timer = setTimeout(() => {
|
|
4356
|
+
signal.removeEventListener("abort", abort);
|
|
4357
|
+
resolve();
|
|
4358
|
+
}, Math.max(5, Math.min(60, r.poll_after_seconds || 30)) * 1e3);
|
|
4359
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4360
|
+
});
|
|
4361
|
+
latest = await get(signal);
|
|
4362
|
+
}
|
|
4363
|
+
} catch (error) {
|
|
4364
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
4365
|
+
if (deadline.aborted && latest) return result("timed_out");
|
|
4366
|
+
throw error;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4034
4370
|
// src/key-tier.ts
|
|
4035
4371
|
var AGENT_HEAD = "pk_agent_";
|
|
4036
4372
|
function parseKeyTier(apiKey) {
|
|
@@ -4115,8 +4451,8 @@ var InboxHandle = class {
|
|
|
4115
4451
|
* Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
|
|
4116
4452
|
* human has to approve it and NOTHING has been delivered yet; anything else was
|
|
4117
4453
|
* delivered. Under the default `require_review` policy a call WITHOUT an
|
|
4118
|
-
* `intent` raises `IntentRequiredError` (422) instead
|
|
4119
|
-
* queued
|
|
4454
|
+
* `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
|
|
4455
|
+
* queued: so pass one, or read `inbox.record.effective_review_policy` first.
|
|
4120
4456
|
*/
|
|
4121
4457
|
send(req, signal) {
|
|
4122
4458
|
return this.transport.send(this.ref, req, signal);
|
|
@@ -4126,7 +4462,7 @@ var InboxHandle = class {
|
|
|
4126
4462
|
* the latest message) or `message_id` (reply to that message); the server
|
|
4127
4463
|
* derives To / Subject / In-Reply-To / References. Set `reply_all` to reply to
|
|
4128
4464
|
* every thread recipient. Returns the same three-way {@link SendOutcome} as
|
|
4129
|
-
* {@link send}
|
|
4465
|
+
* {@link send}: a reply is governed by the review policy too.
|
|
4130
4466
|
*/
|
|
4131
4467
|
reply(req, signal) {
|
|
4132
4468
|
return this.transport.reply(this.ref, req, signal);
|
|
@@ -4135,7 +4471,7 @@ var InboxHandle = class {
|
|
|
4135
4471
|
* Forward a message in this inbox to new recipients, preserving the original.
|
|
4136
4472
|
*
|
|
4137
4473
|
* 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
|
|
4474
|
+
* inbound thread, so it is governed by the review policy exactly like a send :
|
|
4139
4475
|
* same {@link SendOutcome} union, same `intent` requirement.
|
|
4140
4476
|
*/
|
|
4141
4477
|
forward(messageId, req, signal) {
|
|
@@ -4299,7 +4635,7 @@ var ListPage = class _ListPage {
|
|
|
4299
4635
|
this.nextCursor = raw.next_cursor;
|
|
4300
4636
|
}
|
|
4301
4637
|
/**
|
|
4302
|
-
* Fetch the next page. Throws if there is none
|
|
4638
|
+
* Fetch the next page. Throws if there is none - guard with {@link hasMore}.
|
|
4303
4639
|
*/
|
|
4304
4640
|
async nextPage(signal) {
|
|
4305
4641
|
if (!this.hasMore || this.nextCursor === null) {
|
|
@@ -4387,8 +4723,8 @@ var ProjectInboxes = class {
|
|
|
4387
4723
|
* Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
|
|
4388
4724
|
* human has to approve it and NOTHING has been delivered yet; anything else was
|
|
4389
4725
|
* delivered. Under the default `require_review` policy a call WITHOUT an
|
|
4390
|
-
* `intent` raises `IntentRequiredError` (422) instead
|
|
4391
|
-
* queued
|
|
4726
|
+
* `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
|
|
4727
|
+
* queued: so pass one, or read `inbox.record.effective_review_policy` first.
|
|
4392
4728
|
*/
|
|
4393
4729
|
send(projectId, inboxId, req, signal) {
|
|
4394
4730
|
return this.ctx.transport.send(this.ref(projectId, inboxId), req, signal);
|
|
@@ -4454,14 +4790,14 @@ var ProjectInboxes = class {
|
|
|
4454
4790
|
*
|
|
4455
4791
|
* The frozen contract project-prefixes ONLY the inbox collection/item/credentials
|
|
4456
4792
|
* routes (`/v1/projects/{project_id}/inboxes[/{inbox_id}][/credentials]`); the
|
|
4457
|
-
* send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path
|
|
4793
|
+
* send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path :
|
|
4458
4794
|
* they address the inbox by its opaque id directly (`/v1/inboxes/{inbox_id}/…`),
|
|
4459
4795
|
* where the project is implicit in (and enforced by) the inbox id server-side.
|
|
4460
4796
|
*
|
|
4461
4797
|
* So for these sub-ops `projectId` cannot be carried on the URL and is NOT a URL
|
|
4462
4798
|
* selector. The adversarial review flagged that silently discarding it makes the
|
|
4463
4799
|
* signature misleading. CHOICE: keep the arg (dropping it would break the chain's
|
|
4464
|
-
* symmetry with create/list/get/update/delete
|
|
4800
|
+
* symmetry with create/list/get/update/delete: the more disruptive option) but
|
|
4465
4801
|
* VALIDATE it rather than ignore it. We reject the two client mistakes we can catch
|
|
4466
4802
|
* without a round-trip:
|
|
4467
4803
|
* - a blank / whitespace-only `projectId` (a required selector everywhere else in
|
|
@@ -4500,8 +4836,9 @@ var Inboxes = class {
|
|
|
4500
4836
|
this.ctx = ctx;
|
|
4501
4837
|
}
|
|
4502
4838
|
/**
|
|
4503
|
-
* Create an inbox. The default path
|
|
4504
|
-
* `
|
|
4839
|
+
* Create an inbox. The default path creates an address on `extrovertmail.com`
|
|
4840
|
+
* for paid accounts or `free.extrovertmail.com` for free signups, so it returns
|
|
4841
|
+
* a live inbox in one call.
|
|
4505
4842
|
*
|
|
4506
4843
|
* Pass `metadata` to attach arbitrary key-value data, and `client_id` for idempotent creation
|
|
4507
4844
|
* (re-calling with the same id returns the same inbox, with its metadata replayed verbatim).
|
|
@@ -4511,12 +4848,12 @@ var Inboxes = class {
|
|
|
4511
4848
|
return new InboxHandle(this.ctx.transport, inbox.address, this.ctx.handleOptions, inbox);
|
|
4512
4849
|
}
|
|
4513
4850
|
/**
|
|
4514
|
-
* List inboxes visible to the calling key (the bare curl-sugar surface
|
|
4851
|
+
* List inboxes visible to the calling key (the bare curl-sugar surface: resolves
|
|
4515
4852
|
* to the key's default project). An org-tier key has no single default project, so
|
|
4516
4853
|
* the bare list is ambiguous: fail fast client-side with a BreadthRequiredError that
|
|
4517
4854
|
* names the next call, matching the MCP surface, instead of round-tripping to a 400.
|
|
4518
4855
|
* Use `extrovert.projects.inboxes.list("<project_id>")` or `"-"` (org subtree) for
|
|
4519
|
-
* an org key. The check is advisory
|
|
4856
|
+
* an org key. The check is advisory: the server stays authoritative.
|
|
4520
4857
|
*/
|
|
4521
4858
|
list(params = {}, signal) {
|
|
4522
4859
|
if (tierNeedsExplicitBreadth(this.ctx.keyTier)) {
|
|
@@ -4612,10 +4949,22 @@ var Threads = class {
|
|
|
4612
4949
|
constructor(ctx) {
|
|
4613
4950
|
this.ctx = ctx;
|
|
4614
4951
|
}
|
|
4952
|
+
/** List conversations newest-active first. Pass `next_cursor` back as `cursor` for the next page. */
|
|
4953
|
+
list(inbox, params = {}, signal) {
|
|
4954
|
+
return this.ctx.transport.listThreads(inbox, params, signal);
|
|
4955
|
+
}
|
|
4956
|
+
/** Search thread subjects, snippets, and participants. Cursor pagination matches {@link list}. */
|
|
4957
|
+
search(inbox, params, signal) {
|
|
4958
|
+
return this.ctx.transport.searchThreads(inbox, params, signal);
|
|
4959
|
+
}
|
|
4615
4960
|
/** Fetch one thread (+ its messages, oldest-first) by id under its owning inbox address. */
|
|
4616
4961
|
get(inbox, threadId, signal) {
|
|
4617
4962
|
return this.ctx.transport.getThread(inbox, threadId, signal);
|
|
4618
4963
|
}
|
|
4964
|
+
/** Reply in a thread; recipients and RFC reply headers are derived server-side. */
|
|
4965
|
+
reply(inbox, req, signal) {
|
|
4966
|
+
return this.ctx.transport.reply(inbox, req, signal);
|
|
4967
|
+
}
|
|
4619
4968
|
/**
|
|
4620
4969
|
* Delete an entire thread (every message): move to Trash (default) or
|
|
4621
4970
|
* permanently remove when `expunge` is true. `inbox` is the owning address.
|
|
@@ -4677,7 +5026,7 @@ var Suppressions = class {
|
|
|
4677
5026
|
}
|
|
4678
5027
|
/**
|
|
4679
5028
|
* Pre-check whether the caller's org already suppresses a recipient, BEFORE
|
|
4680
|
-
* composing. `suppressed: true` means a send to them would be rejected
|
|
5029
|
+
* composing. `suppressed: true` means a send to them would be rejected: skip
|
|
4681
5030
|
* that recipient. Returns the matching org rows too (never a global/shared row).
|
|
4682
5031
|
*/
|
|
4683
5032
|
precheck(recipient, signal) {
|
|
@@ -4701,17 +5050,25 @@ var Domains = class {
|
|
|
4701
5050
|
this.ctx = ctx;
|
|
4702
5051
|
}
|
|
4703
5052
|
/** List the customer's onboarded domains and their status. */
|
|
4704
|
-
list(signal) {
|
|
4705
|
-
return this.ctx.transport.listDomains(
|
|
5053
|
+
list(paramsOrSignal = {}, signal) {
|
|
5054
|
+
if ("aborted" in paramsOrSignal) return this.ctx.transport.listDomains(paramsOrSignal);
|
|
5055
|
+
return this.ctx.transport.listDomains(signal, paramsOrSignal);
|
|
4706
5056
|
}
|
|
4707
|
-
/** Get one domain's detail
|
|
5057
|
+
/** Get one domain's detail, verification status, and nameserver records. */
|
|
4708
5058
|
get(domain, signal) {
|
|
4709
5059
|
return this.ctx.transport.getDomain(domain, signal);
|
|
4710
5060
|
}
|
|
5061
|
+
/** Wait up to 50 seconds, then return an explicit resumable outcome. No DNS writes. */
|
|
5062
|
+
wait(domain, options = {}) {
|
|
5063
|
+
return waitForDomain((signal) => this.ctx.transport.getDomain(domain, signal), options);
|
|
5064
|
+
}
|
|
5065
|
+
/** Resume durable updates for this domain using the previous next_cursor as after. */
|
|
5066
|
+
events(domain, params = {}, signal) {
|
|
5067
|
+
return this.ctx.transport.listDomainEvents(domain, params, signal);
|
|
5068
|
+
}
|
|
4711
5069
|
/**
|
|
4712
|
-
*
|
|
4713
|
-
*
|
|
4714
|
-
* the record set / NS instruction.
|
|
5070
|
+
* Add a delegated inbox domain the customer controls. Returns the nameserver
|
|
5071
|
+
* records to publish and never spends money.
|
|
4715
5072
|
*/
|
|
4716
5073
|
onboard(req, signal) {
|
|
4717
5074
|
return this.ctx.transport.onboardDomain(req, signal);
|
|
@@ -4730,6 +5087,46 @@ var Domains = class {
|
|
|
4730
5087
|
return this.ctx.transport.offboardDomain(domain, signal);
|
|
4731
5088
|
}
|
|
4732
5089
|
};
|
|
5090
|
+
var Commerce = class {
|
|
5091
|
+
constructor(ctx) {
|
|
5092
|
+
this.ctx = ctx;
|
|
5093
|
+
}
|
|
5094
|
+
requireIdempotencyKey(value) {
|
|
5095
|
+
if (value.trim().length < 8) {
|
|
5096
|
+
throw new ValidationError({
|
|
5097
|
+
status: 400,
|
|
5098
|
+
code: "bad_request",
|
|
5099
|
+
message: "idempotency_key must be a stable value of at least 8 characters; reuse it for retries of the same intent."
|
|
5100
|
+
});
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
/** Quote a domain without purchasing, reserving, or approving it. */
|
|
5104
|
+
quoteDomain(req, signal) {
|
|
5105
|
+
return this.ctx.transport.quoteDomain(req, signal);
|
|
5106
|
+
}
|
|
5107
|
+
/** Create a durable domain-purchase request for human approval. */
|
|
5108
|
+
requestDomainPurchase(req, signal) {
|
|
5109
|
+
this.requireIdempotencyKey(req.idempotency_key);
|
|
5110
|
+
return this.ctx.transport.requestDomainPurchase(req, signal);
|
|
5111
|
+
}
|
|
5112
|
+
/** Create a durable plan-upgrade or downgrade request for human approval. */
|
|
5113
|
+
requestPlanChange(req, signal) {
|
|
5114
|
+
this.requireIdempotencyKey(req.idempotency_key);
|
|
5115
|
+
return this.ctx.transport.requestPlanChange(req, signal);
|
|
5116
|
+
}
|
|
5117
|
+
/** Poll one request's exact blockers, approval URL, and next-action guidance. */
|
|
5118
|
+
get(requestId, signal) {
|
|
5119
|
+
return this.ctx.transport.getCommerceRequest(requestId, signal);
|
|
5120
|
+
}
|
|
5121
|
+
/** Withdraw this agent's request while its durable state still permits cancellation. */
|
|
5122
|
+
cancel(requestId, signal) {
|
|
5123
|
+
return this.ctx.transport.cancelCommerceRequest(requestId, signal);
|
|
5124
|
+
}
|
|
5125
|
+
/** List visible commerce requests using the API's opaque page token. */
|
|
5126
|
+
list(params = {}, signal) {
|
|
5127
|
+
return this.ctx.transport.listCommerceRequests(params, signal);
|
|
5128
|
+
}
|
|
5129
|
+
};
|
|
4733
5130
|
var Reviews = class {
|
|
4734
5131
|
constructor(ctx) {
|
|
4735
5132
|
this.ctx = ctx;
|
|
@@ -4750,7 +5147,7 @@ var Reviews = class {
|
|
|
4750
5147
|
/**
|
|
4751
5148
|
* Get the human's assembled feedback (M5): the diff + comments + decision + the
|
|
4752
5149
|
* rules born from this review. Read it after a rejected/edited nudge to learn what
|
|
4753
|
-
* the human wanted. $0 LLM
|
|
5150
|
+
* the human wanted. $0 LLM: pure assembly on our side.
|
|
4754
5151
|
*/
|
|
4755
5152
|
feedback(reviewId, signal) {
|
|
4756
5153
|
return this.ctx.transport.getReviewFeedback(reviewId, signal);
|
|
@@ -4758,7 +5155,7 @@ var Reviews = class {
|
|
|
4758
5155
|
/**
|
|
4759
5156
|
* Post a chat turn on a review's thread (M5): an agent question to the human
|
|
4760
5157
|
* reviewer; flips in_review -> chatting on the first turn. Idempotent on the
|
|
4761
|
-
* optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM
|
|
5158
|
+
* optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM: you compose it.
|
|
4762
5159
|
*/
|
|
4763
5160
|
chat(reviewId, req, idempotencyKey, signal) {
|
|
4764
5161
|
return this.ctx.transport.postReviewChat(reviewId, req, idempotencyKey, signal);
|
|
@@ -4766,8 +5163,8 @@ var Reviews = class {
|
|
|
4766
5163
|
/**
|
|
4767
5164
|
* Post a new agent draft under a parent_revision CAS (M5; D17). parent_revision
|
|
4768
5165
|
* must equal the draft's current revision, else a 409 STALE with NO mutation (the
|
|
4769
|
-
* human always wins
|
|
4770
|
-
* in place (revision++) and returns to needs_review. $0 LLM
|
|
5166
|
+
* human always wins: re-read, re-apply, retry). On success the draft is re-rendered
|
|
5167
|
+
* in place (revision++) and returns to needs_review. $0 LLM: you compose the redraft.
|
|
4771
5168
|
*/
|
|
4772
5169
|
revise(reviewId, req, signal) {
|
|
4773
5170
|
return this.ctx.transport.submitRevision(reviewId, req, signal);
|
|
@@ -4786,9 +5183,9 @@ var Reviews = class {
|
|
|
4786
5183
|
* assert "I reviewed this against rules vX and no change is needed", advancing the
|
|
4787
5184
|
* draft's composed_* versions with no new draft, no revision bump, no nudge. A
|
|
4788
5185
|
* born-stale draft re-stamped to the current version becomes current-enough and
|
|
4789
|
-
* releasable on the next reconciliation sweep
|
|
5186
|
+
* releasable on the next reconciliation sweep: the cheap counterpart to revise().
|
|
4790
5187
|
* against_version above the category's current rules-version is 400; a terminal draft
|
|
4791
|
-
* 409s. $0 LLM
|
|
5188
|
+
* 409s. $0 LLM: you judged.
|
|
4792
5189
|
*/
|
|
4793
5190
|
restamp(reviewId, req, signal) {
|
|
4794
5191
|
return this.ctx.transport.restampReview(reviewId, req, signal);
|
|
@@ -4807,13 +5204,13 @@ var Reviews = class {
|
|
|
4807
5204
|
}
|
|
4808
5205
|
/**
|
|
4809
5206
|
* Submit a reviewer decision (M8 Slice B; reviewer_decide, D5/§9). approve/edit → the
|
|
4810
|
-
* PLATFORM
|
|
4811
|
-
* mailbox:send on an inbox it doesn't own
|
|
5207
|
+
* PLATFORM sends with the COMPOSER's credentials (the reviewer NEVER holds
|
|
5208
|
+
* mailbox:send on an inbox it doesn't own: the credential boundary); reject → back to
|
|
4812
5209
|
* the composer (needs_review, hop_count++); escalate → the human queue. revision/
|
|
4813
|
-
* version are the CAS (409 STALE on mismatch, NO mutation
|
|
5210
|
+
* version are the CAS (409 STALE on mismatch, NO mutation: the human always wins,
|
|
4814
5211
|
* D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline)
|
|
4815
|
-
* FORCE a reject to the human regardless of intent
|
|
4816
|
-
* LLM
|
|
5212
|
+
* FORCE a reject to the human regardless of intent: `forced_by_breaker` names it. $0
|
|
5213
|
+
* LLM: you judged; we route, send, and enforce the breakers.
|
|
4817
5214
|
*/
|
|
4818
5215
|
decide(reviewId, req, signal) {
|
|
4819
5216
|
return this.ctx.transport.reviewerDecide(reviewId, req, signal);
|
|
@@ -4852,14 +5249,14 @@ var Categories = class {
|
|
|
4852
5249
|
propose(req, signal) {
|
|
4853
5250
|
return this.ctx.transport.proposeCategory(req, signal);
|
|
4854
5251
|
}
|
|
4855
|
-
/** Rename / re-describe a category
|
|
5252
|
+
/** Rename / re-describe a category: metadata only (D10). */
|
|
4856
5253
|
update(categoryId, req, signal) {
|
|
4857
5254
|
return this.ctx.transport.updateCategory(categoryId, req, signal);
|
|
4858
5255
|
}
|
|
4859
5256
|
/**
|
|
4860
5257
|
* Read the effective risk dial (D4/D12): the account default + every category's
|
|
4861
5258
|
* overrides (each with its resolved effective value; null override = inherit).
|
|
4862
|
-
* Read-only
|
|
5259
|
+
* Read-only: agents read but NEVER flip the dial; setting it is a human (console)
|
|
4863
5260
|
* action (D16).
|
|
4864
5261
|
*/
|
|
4865
5262
|
riskDial(signal) {
|
|
@@ -4875,7 +5272,7 @@ var Categories = class {
|
|
|
4875
5272
|
}
|
|
4876
5273
|
/**
|
|
4877
5274
|
* 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
|
|
5275
|
+
* returns the current gate status. It does NOT change the category state: flipping
|
|
4879
5276
|
* the bit is a human (console) action; an agent only proposes.
|
|
4880
5277
|
*/
|
|
4881
5278
|
proposeGraduation(categoryId, req = {}, signal) {
|
|
@@ -4884,7 +5281,7 @@ var Categories = class {
|
|
|
4884
5281
|
/**
|
|
4885
5282
|
* Read the D19/§8 backlog-reconciliation status: how many of the category's QUEUED
|
|
4886
5283
|
* drafts are stale vs current-enough against the current rules-version (a pure
|
|
4887
|
-
* integer compare, $0 LLM). Read-only
|
|
5284
|
+
* integer compare, $0 LLM). Read-only: you READ the picture; the human (console
|
|
4888
5285
|
* scan-backlog) or the graduate/rule-change hooks TRIGGER the actual reconciliation
|
|
4889
5286
|
* sweep that releases current-enough drafts and nudges stale ones to redraft.
|
|
4890
5287
|
*/
|
|
@@ -4907,6 +5304,10 @@ var Rules = class {
|
|
|
4907
5304
|
constructor(ctx) {
|
|
4908
5305
|
this.ctx = ctx;
|
|
4909
5306
|
}
|
|
5307
|
+
/** Learn category or organization house rules from verified human review feedback. */
|
|
5308
|
+
learnFromReview(reviewId, req, signal) {
|
|
5309
|
+
return this.ctx.transport.learnReviewRule(reviewId, req, signal);
|
|
5310
|
+
}
|
|
4910
5311
|
/** Get the ORDERED active rule set (precedence ladder applied; NO LLM). */
|
|
4911
5312
|
get(params = {}, signal) {
|
|
4912
5313
|
return this.ctx.transport.getRules(params, signal);
|
|
@@ -4914,8 +5315,8 @@ var Rules = class {
|
|
|
4914
5315
|
/**
|
|
4915
5316
|
* Save / edit a rule (append-only by supersession; D11). An agent-plane save is
|
|
4916
5317
|
* ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
|
|
4917
|
-
* key's project.
|
|
4918
|
-
* rules in v1
|
|
5318
|
+
* key's project. For org-layer house rules use learnFromReview with an authenticated human source; this method cannot author (`rule_layer:"org"`)
|
|
5319
|
+
* rules in v1: that is a console/admin action.
|
|
4919
5320
|
*/
|
|
4920
5321
|
save(req, signal) {
|
|
4921
5322
|
return this.ctx.transport.saveRule(req, signal);
|
|
@@ -4924,7 +5325,7 @@ var Rules = class {
|
|
|
4924
5325
|
promote(ruleId, toScope, signal) {
|
|
4925
5326
|
return this.ctx.transport.promoteRule(ruleId, toScope, signal);
|
|
4926
5327
|
}
|
|
4927
|
-
/** Retire a rule
|
|
5328
|
+
/** Retire a rule: soft delete; the history survives as training data. */
|
|
4928
5329
|
retire(ruleId, signal) {
|
|
4929
5330
|
return this.ctx.transport.retireRule(ruleId, signal);
|
|
4930
5331
|
}
|
|
@@ -4932,7 +5333,7 @@ var Rules = class {
|
|
|
4932
5333
|
audit(params = {}, signal) {
|
|
4933
5334
|
return this.ctx.transport.getRuleAudit(params, signal);
|
|
4934
5335
|
}
|
|
4935
|
-
/** Undo a rule change by its audit-row id (udo_…)
|
|
5336
|
+
/** Undo a rule change by its audit-row id (udo_…): restore the prior version. */
|
|
4936
5337
|
undo(udoId, signal) {
|
|
4937
5338
|
return this.ctx.transport.undoRuleChange(udoId, signal);
|
|
4938
5339
|
}
|
|
@@ -4989,26 +5390,29 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
4989
5390
|
this.contactLists = new ContactLists(ctx);
|
|
4990
5391
|
this.suppressions = new Suppressions(ctx);
|
|
4991
5392
|
this.domains = new Domains(ctx);
|
|
5393
|
+
this.commerce = new Commerce(ctx);
|
|
4992
5394
|
this.reviews = new Reviews(ctx);
|
|
4993
5395
|
this.categories = new Categories(ctx);
|
|
4994
5396
|
this.rules = new Rules(ctx);
|
|
4995
5397
|
this.projects = new Projects(ctx);
|
|
4996
5398
|
}
|
|
4997
5399
|
/**
|
|
4998
|
-
* Redeem an enrollment token (`pk_enroll_...`) and
|
|
5400
|
+
* Redeem an enrollment token (`pk_enroll_...`) and issue a scoped agent key.
|
|
4999
5401
|
*
|
|
5000
5402
|
* Idempotent on `agent_handle`: redeeming twice with the same handle returns the same agent.
|
|
5001
|
-
* Returns the raw `EnrollResponse`
|
|
5403
|
+
* Returns the raw `EnrollResponse` - to immediately use the issued key, prefer
|
|
5002
5404
|
* {@link ExtrovertClient.enrolled}.
|
|
5003
5405
|
*/
|
|
5004
5406
|
enroll(req, signal) {
|
|
5005
5407
|
return this.transport.enroll(req, signal);
|
|
5006
5408
|
}
|
|
5007
5409
|
/**
|
|
5008
|
-
*
|
|
5009
|
-
*
|
|
5010
|
-
*
|
|
5011
|
-
*
|
|
5410
|
+
* Request a free account in one unauthenticated call. When free signup is
|
|
5411
|
+
* enabled, this provisions a tenant plus a first inbox and returns a
|
|
5412
|
+
* verification-only agent key. That key can only call {@link verify}; it cannot
|
|
5413
|
+
* read or send mail. A one-time code is emailed to `human_email`. Call
|
|
5414
|
+
* {@link verify} with the code to activate the account and receive full scopes.
|
|
5415
|
+
* Idempotent on `human_email`: re-calling rotates the key and resends the code.
|
|
5012
5416
|
* When free signup is paused, this throws an `ApiError` with status 403 and
|
|
5013
5417
|
* code `signup_disabled` without creating account state.
|
|
5014
5418
|
*/
|
|
@@ -5031,7 +5435,7 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5031
5435
|
return this.transport.whoami(signal);
|
|
5032
5436
|
}
|
|
5033
5437
|
/**
|
|
5034
|
-
* Poll the status of an async job (`GET /v1/jobs/{job_id}`)
|
|
5438
|
+
* Poll the status of an async job (`GET /v1/jobs/{job_id}`) - currently only
|
|
5035
5439
|
* the domain-offboard teardown started by {@link Domains.offboard} enqueues
|
|
5036
5440
|
* one. `status` is terminal on succeeded/failed/cancelled; keep polling
|
|
5037
5441
|
* otherwise. An unknown or foreign job id is a {@link NotFoundError}.
|
|
@@ -5040,8 +5444,8 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5040
5444
|
return this.transport.getJob(jobId, signal);
|
|
5041
5445
|
}
|
|
5042
5446
|
/**
|
|
5043
|
-
* Redeem an enrollment token and return a *new* client already authenticated with the
|
|
5044
|
-
* agent key
|
|
5447
|
+
* Redeem an enrollment token and return a *new* client already authenticated with the issued
|
|
5448
|
+
* agent key - the natural "redeem then act" flow for an agent.
|
|
5045
5449
|
*
|
|
5046
5450
|
* ```ts
|
|
5047
5451
|
* const bootstrap = new Extrovert({ apiKey: enrollmentToken });
|
|
@@ -5062,7 +5466,7 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5062
5466
|
return { client, enrollment };
|
|
5063
5467
|
}
|
|
5064
5468
|
/**
|
|
5065
|
-
* Get an ergonomic handle to an existing inbox by address
|
|
5469
|
+
* Get an ergonomic handle to an existing inbox by address - without an extra round-trip. Use this
|
|
5066
5470
|
* when you already know the address (e.g. from a previous create) and want to send/wait/reply.
|
|
5067
5471
|
* Call {@link InboxHandle.refresh} to load the full record.
|
|
5068
5472
|
*/
|
|
@@ -5189,14 +5593,14 @@ async function signWebhook(secret, body, timestampSeconds) {
|
|
|
5189
5593
|
}
|
|
5190
5594
|
|
|
5191
5595
|
// src/contract.ts
|
|
5192
|
-
var CONTRACT_VERSION = "0.1.0-pre.
|
|
5596
|
+
var CONTRACT_VERSION = "0.1.0-pre.8";
|
|
5193
5597
|
var CONTRACT_MANIFEST = {
|
|
5194
5598
|
name: "extrovert.review-loop",
|
|
5195
5599
|
version: CONTRACT_VERSION,
|
|
5196
5600
|
stability: "provisional",
|
|
5197
5601
|
kind: "sdk+skill-contract",
|
|
5198
5602
|
spec_ref: "hitl-spec.md#11",
|
|
5199
|
-
// §11 core
|
|
5603
|
+
// §11 core - the five canonical example shapes.
|
|
5200
5604
|
core_shapes: ["ReviewIntent", "ReviewFeedback", "DiffJson", "Rule", "ReviewEvent"],
|
|
5201
5605
|
// The FULL published surface (the §11 core plus the rest of M1–M8). Adding a
|
|
5202
5606
|
// name here without a matching re-export (or vice-versa) breaks the drift test.
|
|
@@ -5220,6 +5624,8 @@ var CONTRACT_MANIFEST = {
|
|
|
5220
5624
|
// realtime (M3)
|
|
5221
5625
|
"ReviewEventReason",
|
|
5222
5626
|
"ReviewEventsResult",
|
|
5627
|
+
"LearnReviewRuleRequest",
|
|
5628
|
+
"LearnedReviewRule",
|
|
5223
5629
|
"ReviewEventCursor",
|
|
5224
5630
|
// chat / revision / restamp (M5/M7)
|
|
5225
5631
|
"PostReviewChatRequest",
|
|
@@ -5247,7 +5653,16 @@ var CONTRACT_MANIFEST = {
|
|
|
5247
5653
|
"ReviewerAction",
|
|
5248
5654
|
"ReviewDecisionContext",
|
|
5249
5655
|
"ReviewerDecisionRequest",
|
|
5250
|
-
"ReviewerDecisionResult"
|
|
5656
|
+
"ReviewerDecisionResult",
|
|
5657
|
+
// agent commerce request plane
|
|
5658
|
+
"CommerceBlocker",
|
|
5659
|
+
"QuoteDomainRequest",
|
|
5660
|
+
"DomainQuote",
|
|
5661
|
+
"CommerceRequestKind",
|
|
5662
|
+
"RequestDomainPurchaseRequest",
|
|
5663
|
+
"RequestPlanChangeRequest",
|
|
5664
|
+
"ListCommerceRequestsParams",
|
|
5665
|
+
"CommerceRequest"
|
|
5251
5666
|
],
|
|
5252
5667
|
// The complete Review Loop behavior lives in the send skill; writing-rule
|
|
5253
5668
|
// governance remains independently installable and part of this contract.
|
|
@@ -5263,6 +5678,7 @@ exports.CONTRACT_MANIFEST = CONTRACT_MANIFEST;
|
|
|
5263
5678
|
exports.CONTRACT_VERSION = CONTRACT_VERSION;
|
|
5264
5679
|
exports.CURRENT_API_VERSION = CURRENT_API_VERSION;
|
|
5265
5680
|
exports.Categories = Categories;
|
|
5681
|
+
exports.Commerce = Commerce;
|
|
5266
5682
|
exports.ConflictError = ConflictError;
|
|
5267
5683
|
exports.ConnectionError = ConnectionError;
|
|
5268
5684
|
exports.ContactLists = ContactLists;
|