@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.js
CHANGED
|
@@ -94,7 +94,7 @@ var ReviewConflictError = class extends ConflictError {
|
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
96
|
* Whether retrying the same call could ever succeed. False for every subclass
|
|
97
|
-
* except {@link StaleError} and {@link BornStaleError}
|
|
97
|
+
* except {@link StaleError} and {@link BornStaleError} - and true there only
|
|
98
98
|
* after re-reading and re-applying on top of the other party's change.
|
|
99
99
|
*/
|
|
100
100
|
get isRetryable() {
|
|
@@ -266,7 +266,7 @@ var CURRENT_API_VERSION = "2026-06-23";
|
|
|
266
266
|
var API_VERSION_HEADER = "Extrovert-Version";
|
|
267
267
|
|
|
268
268
|
// src/http.ts
|
|
269
|
-
var SDK_VERSION = "0.1.0-pre.
|
|
269
|
+
var SDK_VERSION = "0.1.0-pre.8";
|
|
270
270
|
function buildUrl(baseUrl, path, query) {
|
|
271
271
|
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
272
272
|
const rel = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -640,7 +640,23 @@ function mailboxQuickstart(address) {
|
|
|
640
640
|
}
|
|
641
641
|
};
|
|
642
642
|
}
|
|
643
|
-
var
|
|
643
|
+
var PAID_SHARED_DOMAIN = "extrovertmail.com";
|
|
644
|
+
var FREE_SHARED_DOMAIN = "free.extrovertmail.com";
|
|
645
|
+
var RESERVED_SHARED_LOCAL_PARTS = /* @__PURE__ */ new Set([
|
|
646
|
+
"postmaster",
|
|
647
|
+
"admin",
|
|
648
|
+
"webadmin",
|
|
649
|
+
"legal",
|
|
650
|
+
"fraudmark",
|
|
651
|
+
"fraudmarc",
|
|
652
|
+
"keith",
|
|
653
|
+
"melissa",
|
|
654
|
+
"richard",
|
|
655
|
+
"sydney",
|
|
656
|
+
"syd",
|
|
657
|
+
"john",
|
|
658
|
+
"johnny"
|
|
659
|
+
]);
|
|
644
660
|
var MOCK_ORG_ID = "org_mock";
|
|
645
661
|
var MOCK_PROJECT_ID = "prj_mock";
|
|
646
662
|
var DEFAULT_DAILY_SEND_LIMIT = 75;
|
|
@@ -657,6 +673,13 @@ function rid(prefix) {
|
|
|
657
673
|
function randomHandle() {
|
|
658
674
|
return `agent${Math.floor(1e3 + Math.random() * 9e3)}`;
|
|
659
675
|
}
|
|
676
|
+
function validatedSharedLocalPart(value) {
|
|
677
|
+
const normalized = value.toLowerCase().trim().replace(/[^a-z0-9._-]/g, "").replace(/^[._-]+|[._-]+$/g, "").slice(0, 40);
|
|
678
|
+
if (normalized.length < 5 || RESERVED_SHARED_LOCAL_PARTS.has(normalized)) {
|
|
679
|
+
throw new ValidationError({ status: 400, code: "invalid", message: "Shared-domain usernames must normalize to at least 5 characters and cannot use a reserved name." });
|
|
680
|
+
}
|
|
681
|
+
return normalized;
|
|
682
|
+
}
|
|
660
683
|
function assertProjectMatch(projectId) {
|
|
661
684
|
if (projectId !== void 0 && projectId !== MOCK_PROJECT_ID) {
|
|
662
685
|
throw new PermissionError({
|
|
@@ -745,6 +768,8 @@ function freshState() {
|
|
|
745
768
|
contactLists: /* @__PURE__ */ new Map(),
|
|
746
769
|
domains: /* @__PURE__ */ new Map(),
|
|
747
770
|
jobs: /* @__PURE__ */ new Map(),
|
|
771
|
+
commerceRequests: /* @__PURE__ */ new Map(),
|
|
772
|
+
commerceByIdempotency: /* @__PURE__ */ new Map(),
|
|
748
773
|
suppressions: seedSuppressions(),
|
|
749
774
|
orgReviewPolicy: "require_review",
|
|
750
775
|
inboxReviewPolicy: /* @__PURE__ */ new Map(),
|
|
@@ -875,7 +900,7 @@ function reviewConflict(code, review, detail) {
|
|
|
875
900
|
}
|
|
876
901
|
} else {
|
|
877
902
|
fields.push(
|
|
878
|
-
{ field: "revision", code: String(review.revision), detail: "current revision
|
|
903
|
+
{ field: "revision", code: String(review.revision), detail: "current revision - use as parent_revision" },
|
|
879
904
|
{ field: "version", code: String(review.version), detail: "current row version" }
|
|
880
905
|
);
|
|
881
906
|
}
|
|
@@ -911,6 +936,8 @@ ${parent.text}`;
|
|
|
911
936
|
var MockBackend = class {
|
|
912
937
|
constructor() {
|
|
913
938
|
this.state = freshState();
|
|
939
|
+
/** Save / edit a rule (mock) - append-only by supersession (D11). */
|
|
940
|
+
this.learnedRules = /* @__PURE__ */ new Map();
|
|
914
941
|
}
|
|
915
942
|
reset() {
|
|
916
943
|
this.state = freshState();
|
|
@@ -934,7 +961,7 @@ var MockBackend = class {
|
|
|
934
961
|
agent_id: agentId,
|
|
935
962
|
agent_key: `pk_agent_proj_${agentId.slice(4)}_${rid("sk").slice(3)}`,
|
|
936
963
|
scopes: ["mailbox:create", "mailbox:read", "mailbox:send", "webhook:write"],
|
|
937
|
-
// The
|
|
964
|
+
// The issued key is bound to the token's resolved org/project; the agent cannot change it.
|
|
938
965
|
org_id: MOCK_ORG_ID,
|
|
939
966
|
project_id: MOCK_PROJECT_ID
|
|
940
967
|
};
|
|
@@ -949,7 +976,7 @@ var MockBackend = class {
|
|
|
949
976
|
const existing = this.state.signupByEmail.get(email);
|
|
950
977
|
const customerId = existing?.customerId ?? `cus_pn_signup_${rid("c").slice(2)}`;
|
|
951
978
|
const agentId = existing?.agentId ?? rid("agt");
|
|
952
|
-
const address = existing?.address ?? `${req.username ?? randomHandle()}
|
|
979
|
+
const address = existing?.address ?? `${validatedSharedLocalPart(req.username ?? randomHandle())}@${FREE_SHARED_DOMAIN}`;
|
|
953
980
|
const otp = "492013";
|
|
954
981
|
this.state.signupByEmail.set(email, { customerId, agentId, address, otp, verified: false });
|
|
955
982
|
return {
|
|
@@ -1005,8 +1032,10 @@ var MockBackend = class {
|
|
|
1005
1032
|
}
|
|
1006
1033
|
}
|
|
1007
1034
|
const metadata = req.metadata ? mergeMetadata({}, req.metadata) : {};
|
|
1008
|
-
const
|
|
1009
|
-
const
|
|
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();
|
|
1010
1039
|
const id = rid("ibx");
|
|
1011
1040
|
const inbox = {
|
|
1012
1041
|
object: "inbox",
|
|
@@ -1021,6 +1050,7 @@ var MockBackend = class {
|
|
|
1021
1050
|
onboarding_mode: req.domain ? "ns_delegated" : "shared",
|
|
1022
1051
|
agent_id: null,
|
|
1023
1052
|
daily_send_limit: DEFAULT_DAILY_SEND_LIMIT,
|
|
1053
|
+
direct_smtp_enabled: false,
|
|
1024
1054
|
webhook_url: req.webhook_url ?? null,
|
|
1025
1055
|
metadata,
|
|
1026
1056
|
created_at: now(),
|
|
@@ -1059,7 +1089,7 @@ var MockBackend = class {
|
|
|
1059
1089
|
* Normalize an inbox ref (opaque id OR address alias) to the canonical address the
|
|
1060
1090
|
* mock keys its message/thread/contact maps on. The SDK now routes inbox ops by the
|
|
1061
1091
|
* canonical opaque `id` when it holds a full record (matching the contract's
|
|
1062
|
-
* canonical-key semantics), so the mock must resolve an id back to its address
|
|
1092
|
+
* canonical-key semantics), so the mock must resolve an id back to its address -
|
|
1063
1093
|
* both key `state.inboxes` (same object), `state.messages` keys by address only.
|
|
1064
1094
|
* Unknown refs pass through unchanged so the existing not-found paths still fire.
|
|
1065
1095
|
*/
|
|
@@ -1258,8 +1288,8 @@ var MockBackend = class {
|
|
|
1258
1288
|
}
|
|
1259
1289
|
/**
|
|
1260
1290
|
* Submit a new message for review (mock). Rides the SAME endpoint as `send` on
|
|
1261
|
-
* the real server, so it is literally the same call here: the resolved policy
|
|
1262
|
-
* not which SDK method you picked
|
|
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
|
|
1263
1293
|
* (`kind:"queued_for_review"`) or delivered.
|
|
1264
1294
|
*/
|
|
1265
1295
|
submitForReview(address, req) {
|
|
@@ -1341,7 +1371,7 @@ var MockBackend = class {
|
|
|
1341
1371
|
this.state.reviews.set(review.id, review);
|
|
1342
1372
|
this.enqueueTerminalNudge(review);
|
|
1343
1373
|
}
|
|
1344
|
-
/** Raw delivery for a send
|
|
1374
|
+
/** Raw delivery for a send - no policy, only reachable from submitOutbound. */
|
|
1345
1375
|
deliverSend(address, req) {
|
|
1346
1376
|
return this.deliverRaw(address, {
|
|
1347
1377
|
to: toArray(req.to),
|
|
@@ -1353,7 +1383,7 @@ var MockBackend = class {
|
|
|
1353
1383
|
attachments: req.attachments
|
|
1354
1384
|
});
|
|
1355
1385
|
}
|
|
1356
|
-
/** Raw delivery for a reply
|
|
1386
|
+
/** Raw delivery for a reply - no policy, only reachable from submitOutbound. */
|
|
1357
1387
|
deliverReply(address, req, env) {
|
|
1358
1388
|
return this.deliverRaw(address, {
|
|
1359
1389
|
to: env.to,
|
|
@@ -1391,6 +1421,13 @@ var MockBackend = class {
|
|
|
1391
1421
|
* the server does before it writes the review row.
|
|
1392
1422
|
*/
|
|
1393
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
|
+
}
|
|
1394
1431
|
const all = this.state.messages.get(address) ?? [];
|
|
1395
1432
|
let parent;
|
|
1396
1433
|
let threadId = req.thread_id;
|
|
@@ -1408,6 +1445,13 @@ var MockBackend = class {
|
|
|
1408
1445
|
message: "thread_id or message_id is required"
|
|
1409
1446
|
});
|
|
1410
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
|
+
}
|
|
1411
1455
|
const to = [];
|
|
1412
1456
|
if (parent) {
|
|
1413
1457
|
to.push(parent.from.email);
|
|
@@ -1666,7 +1710,7 @@ var MockBackend = class {
|
|
|
1666
1710
|
* review id so a test can drive the reviewer decision plane offline. `createdAtMs`
|
|
1667
1711
|
* (optional) backdates created_at so a test can trip the hard review_deadline breaker.
|
|
1668
1712
|
*/
|
|
1669
|
-
seedReviewerHeldReview(opts = { fromAddress: "
|
|
1713
|
+
seedReviewerHeldReview(opts = { fromAddress: "reviewer@extrovertmail.com" }) {
|
|
1670
1714
|
const review = this.createReviewRecord(opts.fromAddress, {
|
|
1671
1715
|
kind: "send",
|
|
1672
1716
|
subject: "Pilot proposal",
|
|
@@ -1772,7 +1816,7 @@ var MockBackend = class {
|
|
|
1772
1816
|
// ---- Category registry (Review Loop, D9/D10) --------------------------
|
|
1773
1817
|
/**
|
|
1774
1818
|
* Browse the registry (mock), newest-first, excluding merged/soft-deleted. `match`
|
|
1775
|
-
* is a pure lexical filter (every token must appear in name+description)
|
|
1819
|
+
* is a pure lexical filter (every token must appear in name+description) - NO LLM,
|
|
1776
1820
|
* mirroring the server.
|
|
1777
1821
|
*/
|
|
1778
1822
|
listCategories(params = {}) {
|
|
@@ -1813,7 +1857,7 @@ var MockBackend = class {
|
|
|
1813
1857
|
this.state.categories.set(cat.id, cat);
|
|
1814
1858
|
return cat;
|
|
1815
1859
|
}
|
|
1816
|
-
/** Rename / re-describe a category (mock)
|
|
1860
|
+
/** Rename / re-describe a category (mock) - metadata only (D10). */
|
|
1817
1861
|
updateCategory(categoryId, req) {
|
|
1818
1862
|
const cat = this.state.categories.get(categoryId);
|
|
1819
1863
|
if (!cat) return void 0;
|
|
@@ -1823,7 +1867,7 @@ var MockBackend = class {
|
|
|
1823
1867
|
this.state.categories.set(cat.id, cat);
|
|
1824
1868
|
return cat;
|
|
1825
1869
|
}
|
|
1826
|
-
// ---- Graduation + risk dial (Review Loop, D16/D6/D17)
|
|
1870
|
+
// ---- Graduation + risk dial (Review Loop, D16/D6/D17) - agent READ + PROPOSE --
|
|
1827
1871
|
/** The mock account-default risk dial (mirrors the server defaults). */
|
|
1828
1872
|
accountDial() {
|
|
1829
1873
|
return {
|
|
@@ -1839,7 +1883,7 @@ var MockBackend = class {
|
|
|
1839
1883
|
/**
|
|
1840
1884
|
* Read the effective risk dial (mock): the account default + every category with an
|
|
1841
1885
|
* inherited (null override) effective dial. The mock category carries no overrides,
|
|
1842
|
-
* so every category inherits
|
|
1886
|
+
* so every category inherits - effective == account.
|
|
1843
1887
|
*/
|
|
1844
1888
|
getRiskDial() {
|
|
1845
1889
|
const account = this.accountDial();
|
|
@@ -1893,7 +1937,7 @@ var MockBackend = class {
|
|
|
1893
1937
|
}
|
|
1894
1938
|
/**
|
|
1895
1939
|
* Propose graduating a category (mock): returns the current gate status without
|
|
1896
|
-
* changing the category state (D16
|
|
1940
|
+
* changing the category state (D16 - an agent can never flip the bit).
|
|
1897
1941
|
*/
|
|
1898
1942
|
proposeGraduation(categoryId, _req) {
|
|
1899
1943
|
return this.getGraduationStatus(categoryId);
|
|
@@ -1902,7 +1946,7 @@ var MockBackend = class {
|
|
|
1902
1946
|
* Read the D19/§8 backlog-reconciliation status (mock): counts the QUEUED drafts in a
|
|
1903
1947
|
* category that are stale vs current-enough against the current rules-version. The
|
|
1904
1948
|
* mock has no per-draft composed_* stamps on its Review fixtures, so every queued
|
|
1905
|
-
* draft reads as current-enough (composed 0 vs current 0)
|
|
1949
|
+
* draft reads as current-enough (composed 0 vs current 0) - the contract shape is
|
|
1906
1950
|
* exercised; the integer-compare logic is covered by the Go tests.
|
|
1907
1951
|
*/
|
|
1908
1952
|
getScanBacklogStatus(categoryId) {
|
|
@@ -1926,7 +1970,7 @@ var MockBackend = class {
|
|
|
1926
1970
|
};
|
|
1927
1971
|
}
|
|
1928
1972
|
/**
|
|
1929
|
-
* Read the demand-driven pacing state (mock
|
|
1973
|
+
* Read the demand-driven pacing state (mock - M7 Slice B/§8): the cursor + effective
|
|
1930
1974
|
* window/ceiling/interval + each queued draft's classification. The mock has no cursor
|
|
1931
1975
|
* (nothing reviewed) and no composed_* stamps, so every queued draft reads in-window-
|
|
1932
1976
|
* fresh until the window fills, then ahead; the contract shape is exercised (the
|
|
@@ -1966,7 +2010,7 @@ var MockBackend = class {
|
|
|
1966
2010
|
const human = r.author_kind === "human" ? 1 : 0;
|
|
1967
2011
|
return [hard, spec, human, r.rev, r.priority];
|
|
1968
2012
|
}
|
|
1969
|
-
/** Get the ORDERED active rule set (mock)
|
|
2013
|
+
/** Get the ORDERED active rule set (mock) - §7 ladder + category-before-general. */
|
|
1970
2014
|
getRules(params = {}) {
|
|
1971
2015
|
const active = [...this.state.rules.values()].filter((r) => r.status === "active");
|
|
1972
2016
|
const byRank = (a, b) => {
|
|
@@ -1998,7 +2042,27 @@ var MockBackend = class {
|
|
|
1998
2042
|
composition_token_expires_at: params.scope ? void 0 : new Date(Date.now() + 6e5).toISOString()
|
|
1999
2043
|
};
|
|
2000
2044
|
}
|
|
2001
|
-
|
|
2045
|
+
learnReviewRule(reviewId, req) {
|
|
2046
|
+
const fingerprint = JSON.stringify({ reviewId, req });
|
|
2047
|
+
const prior = this.learnedRules.get(req.client_id);
|
|
2048
|
+
if (prior) {
|
|
2049
|
+
if (prior.fingerprint !== fingerprint) throw new ValidationError({ status: 409, code: "conflict", message: "learning retry identity changed" });
|
|
2050
|
+
return structuredClone(prior.result);
|
|
2051
|
+
}
|
|
2052
|
+
const turn = (this.state.reviewTurns.get(reviewId) ?? []).find((t) => t.id === req.source_turn_id);
|
|
2053
|
+
if (!turn || turn.actor_kind !== "human" || !turn.actor_id) throw new ValidationError({ status: 403, code: "forbidden_scope", message: "learning requires authenticated human feedback" });
|
|
2054
|
+
const rule = this.saveRule({ ...req, scope: req.target === "category" ? "category" : "general", source_review_id: reviewId, source_turn_id: turn.id });
|
|
2055
|
+
rule.source_turn_id = turn.id;
|
|
2056
|
+
rule.rule_layer = req.target === "org_house" ? "org" : "project";
|
|
2057
|
+
if (req.target === "org_house") delete rule.project_id;
|
|
2058
|
+
rule.source_review_id = reviewId;
|
|
2059
|
+
rule.source_turn_id = turn.id;
|
|
2060
|
+
const audit = [...this.state.ruleAudit.values()].find((entry) => entry.entity_id === rule.id);
|
|
2061
|
+
audit.after_json = ruleSnapshotJSON(rule);
|
|
2062
|
+
const result = { rule, source_review_id: reviewId, source_turn_id: turn.id, human_id: turn.actor_id, audit_id: audit.id, propagation: "queued" };
|
|
2063
|
+
this.learnedRules.set(req.client_id, { fingerprint, result: structuredClone(result) });
|
|
2064
|
+
return result;
|
|
2065
|
+
}
|
|
2002
2066
|
saveRule(req) {
|
|
2003
2067
|
const text = req.rule_text.trim();
|
|
2004
2068
|
if (!text) {
|
|
@@ -2087,7 +2151,7 @@ var MockBackend = class {
|
|
|
2087
2151
|
this.recordRuleAudit("supersede", next.id, ruleSnapshotJSON(prior), ruleSnapshotJSON(next));
|
|
2088
2152
|
return next;
|
|
2089
2153
|
}
|
|
2090
|
-
/** Retire a rule (mock)
|
|
2154
|
+
/** Retire a rule (mock) - soft delete, or undefined when unknown. */
|
|
2091
2155
|
retireRule(ruleId) {
|
|
2092
2156
|
const rule = this.state.rules.get(ruleId);
|
|
2093
2157
|
if (!rule) return void 0;
|
|
@@ -2105,7 +2169,7 @@ var MockBackend = class {
|
|
|
2105
2169
|
if (params.entity_id) items = items.filter((e) => e.entity_id === params.entity_id);
|
|
2106
2170
|
return { items, total: items.length };
|
|
2107
2171
|
}
|
|
2108
|
-
/** Undo a rule change (mock)
|
|
2172
|
+
/** Undo a rule change (mock) - restore the prior version; idempotent (re-undo 409). */
|
|
2109
2173
|
undoRuleChange(udoId) {
|
|
2110
2174
|
const entry = this.state.ruleAudit.get(udoId);
|
|
2111
2175
|
if (!entry) throw new NotFoundError({ status: 404, code: "not_found", message: "audit row not found" });
|
|
@@ -2209,7 +2273,7 @@ var MockBackend = class {
|
|
|
2209
2273
|
}
|
|
2210
2274
|
}
|
|
2211
2275
|
/**
|
|
2212
|
-
* Enqueue `front_run_next`
|
|
2276
|
+
* Enqueue `front_run_next` - the signal that the review reached a terminal state
|
|
2213
2277
|
* while the agent was still trying to act on it.
|
|
2214
2278
|
*
|
|
2215
2279
|
* Deduped on (review, terminal state, parent revision) so a retry loop hitting
|
|
@@ -2250,8 +2314,8 @@ var MockBackend = class {
|
|
|
2250
2314
|
}
|
|
2251
2315
|
/**
|
|
2252
2316
|
* Mock-only: mirror an approved draft whose delivery then FAILED at the provider.
|
|
2253
|
-
* This is the case the composing agent was previously never told about
|
|
2254
|
-
* console showed the error and the agent's queue stayed silent
|
|
2317
|
+
* This is the case the composing agent was previously never told about - the
|
|
2318
|
+
* console showed the error and the agent's queue stayed silent - so the loop test
|
|
2255
2319
|
* that matters most drives this path.
|
|
2256
2320
|
*/
|
|
2257
2321
|
simulateSendFailed(reviewId, error = "provider rejected the message") {
|
|
@@ -2336,7 +2400,7 @@ var MockBackend = class {
|
|
|
2336
2400
|
}
|
|
2337
2401
|
/**
|
|
2338
2402
|
* Long-poll for a review event (mock). Offline there is nothing to wait FOR, so it
|
|
2339
|
-
* returns the immediate drain (empty when caught up)
|
|
2403
|
+
* returns the immediate drain (empty when caught up) - the server's "empty on
|
|
2340
2404
|
* timeout" contract.
|
|
2341
2405
|
*/
|
|
2342
2406
|
waitForReviewEvent(params = {}) {
|
|
@@ -2425,7 +2489,7 @@ var MockBackend = class {
|
|
|
2425
2489
|
html: opts.html ?? null,
|
|
2426
2490
|
extracted_text: opts.text.trim() || null,
|
|
2427
2491
|
extracted_html: opts.html?.trim() || null,
|
|
2428
|
-
message_id: `<${id}@${address.split("@")[1] ??
|
|
2492
|
+
message_id: `<${id}@${address.split("@")[1] ?? PAID_SHARED_DOMAIN}>`,
|
|
2429
2493
|
folder: opts.direction === "inbound" ? "INBOX" : "Sent",
|
|
2430
2494
|
seen: opts.direction === "outbound",
|
|
2431
2495
|
date: now()
|
|
@@ -2541,10 +2605,10 @@ var MockBackend = class {
|
|
|
2541
2605
|
if (offset + page.length < total) result.next_cursor = String(offset + page.length);
|
|
2542
2606
|
return result;
|
|
2543
2607
|
}
|
|
2544
|
-
listThreads(address) {
|
|
2608
|
+
listThreads(address, params = {}) {
|
|
2545
2609
|
address = this.addrOf(address);
|
|
2546
2610
|
const items = this.threadsFor(address);
|
|
2547
|
-
return
|
|
2611
|
+
return this.paginateThreads(items, params);
|
|
2548
2612
|
}
|
|
2549
2613
|
/** Thread-level search (subject / snippet / participant substring). */
|
|
2550
2614
|
searchThreads(address, params) {
|
|
@@ -2553,7 +2617,17 @@ var MockBackend = class {
|
|
|
2553
2617
|
const items = this.threadsFor(address).filter(
|
|
2554
2618
|
(t) => t.subject.toLowerCase().includes(q) || t.snippet.toLowerCase().includes(q) || t.participants.join(" ").toLowerCase().includes(q)
|
|
2555
2619
|
);
|
|
2556
|
-
return
|
|
2620
|
+
return this.paginateThreads(items, params);
|
|
2621
|
+
}
|
|
2622
|
+
paginateThreads(items, params) {
|
|
2623
|
+
const total = items.length;
|
|
2624
|
+
const cursorOffset = params.cursor === void 0 ? void 0 : Number(params.cursor);
|
|
2625
|
+
const offset = params.offset ?? (Number.isFinite(cursorOffset) ? cursorOffset : 0);
|
|
2626
|
+
const limit = params.limit ?? 25;
|
|
2627
|
+
const page = items.slice(offset, offset + limit);
|
|
2628
|
+
const result = { items: page, total };
|
|
2629
|
+
if (offset + page.length < total) result.next_cursor = String(offset + page.length);
|
|
2630
|
+
return result;
|
|
2557
2631
|
}
|
|
2558
2632
|
/** Fetch one thread (with messages, oldest-first) by id under an inbox. */
|
|
2559
2633
|
getThread(address, threadId) {
|
|
@@ -2625,17 +2699,18 @@ var MockBackend = class {
|
|
|
2625
2699
|
arr.push(m);
|
|
2626
2700
|
byThread.set(m.thread_id, arr);
|
|
2627
2701
|
}
|
|
2628
|
-
const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms));
|
|
2702
|
+
const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms, true));
|
|
2629
2703
|
items.sort((a, b) => b.last_message_at.localeCompare(a.last_message_at));
|
|
2630
2704
|
return items;
|
|
2631
2705
|
}
|
|
2632
|
-
buildThread(address, id, ms) {
|
|
2706
|
+
buildThread(address, id, ms, summary = false) {
|
|
2633
2707
|
const sorted = [...ms].sort((a, b) => a.date.localeCompare(b.date));
|
|
2634
2708
|
const last = sorted[sorted.length - 1];
|
|
2635
2709
|
const seen = /* @__PURE__ */ new Set();
|
|
2636
2710
|
const participants = [];
|
|
2637
|
-
|
|
2638
|
-
|
|
2711
|
+
const participantMessages = summary ? [last] : sorted;
|
|
2712
|
+
for (const m of participantMessages) {
|
|
2713
|
+
for (const a of [m.from, ...m.to, ...m.cc ?? [], ...m.reply_to ?? []]) {
|
|
2639
2714
|
const key = a.email.toLowerCase();
|
|
2640
2715
|
if (seen.has(key)) continue;
|
|
2641
2716
|
seen.add(key);
|
|
@@ -2649,7 +2724,10 @@ var MockBackend = class {
|
|
|
2649
2724
|
participants,
|
|
2650
2725
|
message_count: sorted.length,
|
|
2651
2726
|
last_message_at: last.date,
|
|
2652
|
-
snippet: (last.text ?? last.html ?? "").slice(0, 120)
|
|
2727
|
+
snippet: (last.text ?? last.html ?? "").slice(0, 120),
|
|
2728
|
+
unread: !last.seen,
|
|
2729
|
+
last_message_has_attachments: (this.state.attachments.get(last.id)?.length ?? 0) > 0,
|
|
2730
|
+
last_message_id: last.id
|
|
2653
2731
|
};
|
|
2654
2732
|
}
|
|
2655
2733
|
/**
|
|
@@ -2809,24 +2887,34 @@ ${text}`)) {
|
|
|
2809
2887
|
return this.state.contactLists.delete(entryId);
|
|
2810
2888
|
}
|
|
2811
2889
|
// ---- domains (Slice 5) ------------------------------------------------
|
|
2812
|
-
/**
|
|
2890
|
+
/** Add a delegated domain and return only the customer-published nameservers. */
|
|
2813
2891
|
onboardDomain(req) {
|
|
2814
2892
|
assertProjectMatch(req.project_id);
|
|
2815
2893
|
const name = req.domain.trim().toLowerCase();
|
|
2816
2894
|
const existing = this.state.domains.get(name);
|
|
2817
2895
|
if (existing) return { ...existing };
|
|
2818
|
-
const mode =
|
|
2896
|
+
const mode = "ns_delegated";
|
|
2819
2897
|
const domain = {
|
|
2820
2898
|
id: rid("dom"),
|
|
2821
2899
|
domain: name,
|
|
2822
2900
|
mode,
|
|
2823
|
-
verification_status:
|
|
2824
|
-
dkim_status:
|
|
2825
|
-
shared:
|
|
2901
|
+
verification_status: "verifying",
|
|
2902
|
+
dkim_status: "configured",
|
|
2903
|
+
shared: false,
|
|
2826
2904
|
created_at: now(),
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2905
|
+
delegation_ns: domainDelegationNS(name),
|
|
2906
|
+
instruction: "Add the nameserver entries at your domain provider. We check automatically; use Recheck DNS for an immediate check.",
|
|
2907
|
+
readiness: {
|
|
2908
|
+
status: "waiting_for_dns",
|
|
2909
|
+
label: "Waiting for DNS",
|
|
2910
|
+
summary: "We have not confirmed your nameserver entries yet. Add them at your domain provider; we will finish setup automatically.",
|
|
2911
|
+
reason: "dns_entries_unconfirmed",
|
|
2912
|
+
action_required_by: "customer",
|
|
2913
|
+
next_action: "check_dns_entries",
|
|
2914
|
+
ready_for_inboxes: false,
|
|
2915
|
+
poll_after_seconds: 30,
|
|
2916
|
+
inboxes: { scope: "agent", total: 0, ready: 0, setting_up: 0, needs_attention: 0 }
|
|
2917
|
+
}
|
|
2830
2918
|
};
|
|
2831
2919
|
this.state.domains.set(name, domain);
|
|
2832
2920
|
return { ...domain };
|
|
@@ -2874,6 +2962,129 @@ ${text}`)) {
|
|
|
2874
2962
|
const job = this.state.jobs.get(jobId);
|
|
2875
2963
|
return job ? { ...job } : void 0;
|
|
2876
2964
|
}
|
|
2965
|
+
// ---- commerce (quote/request/status; no human approval mutation) ------
|
|
2966
|
+
quoteDomain(req) {
|
|
2967
|
+
const domain = req.domain.trim().toLowerCase();
|
|
2968
|
+
return {
|
|
2969
|
+
object: "domain_quote",
|
|
2970
|
+
domain,
|
|
2971
|
+
available: !domain.startsWith("unavailable."),
|
|
2972
|
+
currency: "usd",
|
|
2973
|
+
quote_cents: 2500,
|
|
2974
|
+
renewal_cents: 2500,
|
|
2975
|
+
premium: false,
|
|
2976
|
+
quote_expires_at: new Date(Date.now() + 15 * 60 * 1e3).toISOString(),
|
|
2977
|
+
blockers: []
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
requestDomainPurchase(req) {
|
|
2981
|
+
const idem = `domain_purchase:${req.idempotency_key.trim()}`;
|
|
2982
|
+
const replayId = this.state.commerceByIdempotency.get(idem);
|
|
2983
|
+
if (replayId) return { ...this.state.commerceRequests.get(replayId) };
|
|
2984
|
+
const quote = this.quoteDomain({ domain: req.domain });
|
|
2985
|
+
const timestamp = now();
|
|
2986
|
+
const id = rid("creq");
|
|
2987
|
+
const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
|
|
2988
|
+
const request = {
|
|
2989
|
+
object: "commerce_request",
|
|
2990
|
+
id,
|
|
2991
|
+
kind: "domain_purchase",
|
|
2992
|
+
state: "awaiting_human_approval",
|
|
2993
|
+
domain: quote.domain,
|
|
2994
|
+
domain_scope: req.scope ?? "org",
|
|
2995
|
+
rationale: req.rationale,
|
|
2996
|
+
currency: quote.currency,
|
|
2997
|
+
quote_cents: quote.quote_cents,
|
|
2998
|
+
renewal_cents: quote.renewal_cents,
|
|
2999
|
+
quote_expires_at: quote.quote_expires_at,
|
|
3000
|
+
auto_renew: req.auto_renew ?? true,
|
|
3001
|
+
blocker_code: "human_approval_required",
|
|
3002
|
+
blockers: [
|
|
3003
|
+
{
|
|
3004
|
+
code: "human_approval_required",
|
|
3005
|
+
message: "A human billing administrator must approve this domain purchase.",
|
|
3006
|
+
manage_url: approvalUrl
|
|
3007
|
+
}
|
|
3008
|
+
],
|
|
3009
|
+
approval_url: approvalUrl,
|
|
3010
|
+
agent_next_action: "Share approval_url with the human, then poll this request after approval.",
|
|
3011
|
+
retry_safe: true,
|
|
3012
|
+
poll_after_seconds: 10,
|
|
3013
|
+
version: 1,
|
|
3014
|
+
created_at: timestamp,
|
|
3015
|
+
updated_at: timestamp
|
|
3016
|
+
};
|
|
3017
|
+
this.state.commerceRequests.set(id, request);
|
|
3018
|
+
this.state.commerceByIdempotency.set(idem, id);
|
|
3019
|
+
return { ...request };
|
|
3020
|
+
}
|
|
3021
|
+
requestPlanChange(req) {
|
|
3022
|
+
const idem = `plan_change:${req.idempotency_key.trim()}`;
|
|
3023
|
+
const replayId = this.state.commerceByIdempotency.get(idem);
|
|
3024
|
+
if (replayId) return { ...this.state.commerceRequests.get(replayId) };
|
|
3025
|
+
const timestamp = now();
|
|
3026
|
+
const id = rid("creq");
|
|
3027
|
+
const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
|
|
3028
|
+
const request = {
|
|
3029
|
+
object: "commerce_request",
|
|
3030
|
+
id,
|
|
3031
|
+
kind: "plan_change",
|
|
3032
|
+
state: "awaiting_human_approval",
|
|
3033
|
+
target_plan: req.target_plan,
|
|
3034
|
+
current_plan: "developer",
|
|
3035
|
+
rationale: req.rationale,
|
|
3036
|
+
currency: "usd",
|
|
3037
|
+
quote_cents: 0,
|
|
3038
|
+
renewal_cents: 0,
|
|
3039
|
+
auto_renew: true,
|
|
3040
|
+
blocker_code: "human_approval_required",
|
|
3041
|
+
blockers: [
|
|
3042
|
+
{
|
|
3043
|
+
code: "human_approval_required",
|
|
3044
|
+
message: "A human billing administrator must approve this plan change.",
|
|
3045
|
+
manage_url: approvalUrl
|
|
3046
|
+
}
|
|
3047
|
+
],
|
|
3048
|
+
approval_url: approvalUrl,
|
|
3049
|
+
agent_next_action: "Share approval_url with the human, then poll this request after approval.",
|
|
3050
|
+
retry_safe: true,
|
|
3051
|
+
poll_after_seconds: 10,
|
|
3052
|
+
version: 1,
|
|
3053
|
+
created_at: timestamp,
|
|
3054
|
+
updated_at: timestamp
|
|
3055
|
+
};
|
|
3056
|
+
this.state.commerceRequests.set(id, request);
|
|
3057
|
+
this.state.commerceByIdempotency.set(idem, id);
|
|
3058
|
+
return { ...request };
|
|
3059
|
+
}
|
|
3060
|
+
getCommerceRequest(requestId) {
|
|
3061
|
+
const request = this.state.commerceRequests.get(requestId);
|
|
3062
|
+
return request ? { ...request } : void 0;
|
|
3063
|
+
}
|
|
3064
|
+
cancelCommerceRequest(requestId) {
|
|
3065
|
+
const request = this.state.commerceRequests.get(requestId);
|
|
3066
|
+
if (!request) return void 0;
|
|
3067
|
+
if (!["awaiting_human_approval", "blocked", "approved", "payment_action_required", "payment_failed"].includes(request.state)) {
|
|
3068
|
+
throw new ValidationError({ status: 409, code: "conflict", message: "this request can no longer be cancelled" });
|
|
3069
|
+
}
|
|
3070
|
+
request.state = "cancelled";
|
|
3071
|
+
request.blocker_code = void 0;
|
|
3072
|
+
request.blockers = [];
|
|
3073
|
+
request.agent_next_action = "The request is cancelled. Create a new request only if the purchase is still needed.";
|
|
3074
|
+
request.version += 1;
|
|
3075
|
+
request.updated_at = now();
|
|
3076
|
+
return { ...request };
|
|
3077
|
+
}
|
|
3078
|
+
listCommerceRequests(params = {}) {
|
|
3079
|
+
let items = [...this.state.commerceRequests.values()];
|
|
3080
|
+
items.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
3081
|
+
const total = items.length;
|
|
3082
|
+
const offset = params.page ? Math.max(0, Number.parseInt(params.page, 10) || 0) : 0;
|
|
3083
|
+
const limit = Math.max(1, Math.min(params.limit ?? 50, 100));
|
|
3084
|
+
const page = items.slice(offset, offset + limit).map((request) => ({ ...request }));
|
|
3085
|
+
const next = offset + page.length;
|
|
3086
|
+
return { items: page, total, next_cursor: next < total ? String(next) : void 0 };
|
|
3087
|
+
}
|
|
2877
3088
|
// ---- suppressions (recipient opt-outs / list-unsubscribe) --------------
|
|
2878
3089
|
/**
|
|
2879
3090
|
* Pre-check whether the caller's org suppresses a recipient (mirrors
|
|
@@ -2917,7 +3128,7 @@ ${text}`)) {
|
|
|
2917
3128
|
/**
|
|
2918
3129
|
* Reject the WHOLE send if ANY recipient has an active org-scope suppression,
|
|
2919
3130
|
* naming exactly the suppressed addresses (never the scope/origin) so the caller
|
|
2920
|
-
* can drop them and retry
|
|
3131
|
+
* can drop them and retry - mirroring the live `recipient_suppressed` (422) path.
|
|
2921
3132
|
*/
|
|
2922
3133
|
enforceSuppression(recipients) {
|
|
2923
3134
|
const active = new Set(
|
|
@@ -3000,16 +3211,6 @@ function redactSecret(w) {
|
|
|
3000
3211
|
const { secret: _omit, ...rest } = w;
|
|
3001
3212
|
return rest;
|
|
3002
3213
|
}
|
|
3003
|
-
function domainRecordSet(domain) {
|
|
3004
|
-
const dkimSuffix = domain.replace(/\./g, "-");
|
|
3005
|
-
return [
|
|
3006
|
-
{ name: domain, type: "MX", value: "smtp.extrovert.dev", priority: 10, ttl: 3600 },
|
|
3007
|
-
{ name: domain, type: "TXT", value: "v=spf1 include:spf.protection.outlook.com -all", ttl: 3600 },
|
|
3008
|
-
{ name: `_dmarc.${domain}`, type: "TXT", value: "v=DMARC1; p=none; rua=mailto:dmarc@smtp.extrovert.dev", ttl: 3600 },
|
|
3009
|
-
{ name: `selector1._domainkey.${domain}`, type: "CNAME", value: `selector1-${dkimSuffix}._domainkey.azurecomm.net`, ttl: 3600 },
|
|
3010
|
-
{ name: `selector2._domainkey.${domain}`, type: "CNAME", value: `selector2-${dkimSuffix}._domainkey.azurecomm.net`, ttl: 3600 }
|
|
3011
|
-
];
|
|
3012
|
-
}
|
|
3013
3214
|
function domainDelegationNS(domain) {
|
|
3014
3215
|
return [
|
|
3015
3216
|
{ name: domain, type: "NS", value: "ns1.extrovert.dev", ttl: 300 },
|
|
@@ -3393,8 +3594,11 @@ var HttpTransport = class {
|
|
|
3393
3594
|
signal
|
|
3394
3595
|
});
|
|
3395
3596
|
}
|
|
3396
|
-
listDomains(signal) {
|
|
3397
|
-
return this.call({ method: "GET", path: "/v1/domains", signal });
|
|
3597
|
+
listDomains(signal, params = {}) {
|
|
3598
|
+
return this.call({ method: "GET", path: "/v1/domains", query: params, signal });
|
|
3599
|
+
}
|
|
3600
|
+
listDomainEvents(domain, params, signal) {
|
|
3601
|
+
return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}/events`, query: params, signal });
|
|
3398
3602
|
}
|
|
3399
3603
|
getDomain(domain, signal) {
|
|
3400
3604
|
return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}`, signal });
|
|
@@ -3402,6 +3606,8 @@ var HttpTransport = class {
|
|
|
3402
3606
|
onboardDomain(req, signal) {
|
|
3403
3607
|
return this.call({ method: "POST", path: "/v1/domains", body: req, signal });
|
|
3404
3608
|
}
|
|
3609
|
+
// Delegated domains perform an immediate authoritative DNS check. Inspect
|
|
3610
|
+
// delegation.status separately from mail readiness; 429 requests may be retried.
|
|
3405
3611
|
verifyDomain(domain, signal) {
|
|
3406
3612
|
return this.call({
|
|
3407
3613
|
method: "POST",
|
|
@@ -3426,6 +3632,54 @@ var HttpTransport = class {
|
|
|
3426
3632
|
getJob(jobId, signal) {
|
|
3427
3633
|
return this.call({ method: "GET", path: `/v1/jobs/${encodeURIComponent(jobId)}`, signal });
|
|
3428
3634
|
}
|
|
3635
|
+
quoteDomain(req, signal) {
|
|
3636
|
+
return this.call({ method: "POST", path: "/v1/commerce/domain-quotes", body: req, signal });
|
|
3637
|
+
}
|
|
3638
|
+
requestDomainPurchase(req, signal) {
|
|
3639
|
+
const { idempotency_key, ...body } = req;
|
|
3640
|
+
return this.call({
|
|
3641
|
+
method: "POST",
|
|
3642
|
+
path: "/v1/commerce/requests/domain-purchases",
|
|
3643
|
+
body,
|
|
3644
|
+
idempotencyKey: idempotency_key,
|
|
3645
|
+
signal
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
requestPlanChange(req, signal) {
|
|
3649
|
+
const { idempotency_key, ...body } = req;
|
|
3650
|
+
return this.call({
|
|
3651
|
+
method: "POST",
|
|
3652
|
+
path: "/v1/commerce/requests/plan-changes",
|
|
3653
|
+
body,
|
|
3654
|
+
idempotencyKey: idempotency_key,
|
|
3655
|
+
signal
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
getCommerceRequest(requestId, signal) {
|
|
3659
|
+
return this.call({
|
|
3660
|
+
method: "GET",
|
|
3661
|
+
path: `/v1/commerce/requests/${encodeURIComponent(requestId)}`,
|
|
3662
|
+
signal
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
cancelCommerceRequest(requestId, signal) {
|
|
3666
|
+
return this.call({
|
|
3667
|
+
method: "POST",
|
|
3668
|
+
path: `/v1/commerce/requests/${encodeURIComponent(requestId)}/cancel`,
|
|
3669
|
+
signal
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
listCommerceRequests(params, signal) {
|
|
3673
|
+
return this.call({
|
|
3674
|
+
method: "GET",
|
|
3675
|
+
path: "/v1/commerce/requests",
|
|
3676
|
+
query: {
|
|
3677
|
+
limit: params.limit,
|
|
3678
|
+
page: params.page
|
|
3679
|
+
},
|
|
3680
|
+
signal
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3429
3683
|
submitForReview(address, req, signal) {
|
|
3430
3684
|
return this.call({
|
|
3431
3685
|
method: "POST",
|
|
@@ -3449,6 +3703,7 @@ var HttpTransport = class {
|
|
|
3449
3703
|
state: Array.isArray(params.state) ? params.state.join(",") : params.state,
|
|
3450
3704
|
category_id: params.category_id,
|
|
3451
3705
|
inbox: params.inbox,
|
|
3706
|
+
composer: params.composer,
|
|
3452
3707
|
limit: params.limit,
|
|
3453
3708
|
page: params.page
|
|
3454
3709
|
};
|
|
@@ -3531,10 +3786,12 @@ var HttpTransport = class {
|
|
|
3531
3786
|
});
|
|
3532
3787
|
}
|
|
3533
3788
|
waitForReviewEvent(params, signal) {
|
|
3789
|
+
const waitSeconds = Math.min(55, Math.max(1, params.wait_seconds ?? 55));
|
|
3534
3790
|
return this.call({
|
|
3535
3791
|
method: "GET",
|
|
3536
3792
|
path: "/v1/reviews/events/wait",
|
|
3537
|
-
query: { review_id: params.review_id, limit: params.limit, wait_seconds:
|
|
3793
|
+
query: { review_id: params.review_id, limit: params.limit, wait_seconds: waitSeconds },
|
|
3794
|
+
timeoutMs: (waitSeconds + 10) * 1e3,
|
|
3538
3795
|
signal
|
|
3539
3796
|
});
|
|
3540
3797
|
}
|
|
@@ -3603,6 +3860,9 @@ var HttpTransport = class {
|
|
|
3603
3860
|
signal
|
|
3604
3861
|
});
|
|
3605
3862
|
}
|
|
3863
|
+
learnReviewRule(reviewId, req, signal) {
|
|
3864
|
+
return this.call({ method: "POST", path: `/v1/reviews/${encodeURIComponent(reviewId)}/learned-rules`, body: req, signal });
|
|
3865
|
+
}
|
|
3606
3866
|
saveRule(req, signal) {
|
|
3607
3867
|
return this.call({
|
|
3608
3868
|
method: "PUT",
|
|
@@ -3784,8 +4044,8 @@ var MockTransport = class {
|
|
|
3784
4044
|
async searchMessages(address, params) {
|
|
3785
4045
|
return this.backend.searchMessages(address, params);
|
|
3786
4046
|
}
|
|
3787
|
-
async listThreads(address,
|
|
3788
|
-
return this.backend.listThreads(address);
|
|
4047
|
+
async listThreads(address, params) {
|
|
4048
|
+
return this.backend.listThreads(address, params);
|
|
3789
4049
|
}
|
|
3790
4050
|
async searchThreads(address, params) {
|
|
3791
4051
|
return this.backend.searchThreads(address, params);
|
|
@@ -3849,8 +4109,16 @@ var MockTransport = class {
|
|
|
3849
4109
|
if (!row) throw notFound("suppression", id);
|
|
3850
4110
|
return row;
|
|
3851
4111
|
}
|
|
3852
|
-
async listDomains() {
|
|
3853
|
-
|
|
4112
|
+
async listDomains(_signal, params = {}) {
|
|
4113
|
+
const page = this.backend.listDomains();
|
|
4114
|
+
const offset = Number(params.page ?? 0);
|
|
4115
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid page cursor");
|
|
4116
|
+
const items = page.items.slice(offset, offset + (params.limit ?? 50));
|
|
4117
|
+
return { items, total: page.total, next_cursor: offset + items.length < page.items.length ? String(offset + items.length) : void 0 };
|
|
4118
|
+
}
|
|
4119
|
+
async listDomainEvents(domain, params) {
|
|
4120
|
+
if (!this.backend.getDomain(domain)) throw notFound("domain", domain);
|
|
4121
|
+
return { items: [], next_cursor: params.after ?? "0", has_more: false, poll_after_seconds: 30 };
|
|
3854
4122
|
}
|
|
3855
4123
|
async getDomain(domain) {
|
|
3856
4124
|
const d = this.backend.getDomain(domain);
|
|
@@ -3875,6 +4143,28 @@ var MockTransport = class {
|
|
|
3875
4143
|
if (!job) throw notFound("job", jobId);
|
|
3876
4144
|
return job;
|
|
3877
4145
|
}
|
|
4146
|
+
async quoteDomain(req) {
|
|
4147
|
+
return this.backend.quoteDomain(req);
|
|
4148
|
+
}
|
|
4149
|
+
async requestDomainPurchase(req) {
|
|
4150
|
+
return this.backend.requestDomainPurchase(req);
|
|
4151
|
+
}
|
|
4152
|
+
async requestPlanChange(req) {
|
|
4153
|
+
return this.backend.requestPlanChange(req);
|
|
4154
|
+
}
|
|
4155
|
+
async getCommerceRequest(requestId) {
|
|
4156
|
+
const request = this.backend.getCommerceRequest(requestId);
|
|
4157
|
+
if (!request) throw notFound("commerce request", requestId);
|
|
4158
|
+
return request;
|
|
4159
|
+
}
|
|
4160
|
+
async cancelCommerceRequest(requestId) {
|
|
4161
|
+
const request = this.backend.cancelCommerceRequest(requestId);
|
|
4162
|
+
if (!request) throw notFound("commerce request", requestId);
|
|
4163
|
+
return request;
|
|
4164
|
+
}
|
|
4165
|
+
async listCommerceRequests(params) {
|
|
4166
|
+
return this.backend.listCommerceRequests(params);
|
|
4167
|
+
}
|
|
3878
4168
|
async submitForReview(address, req) {
|
|
3879
4169
|
return this.backend.submitForReview(address, req);
|
|
3880
4170
|
}
|
|
@@ -3980,6 +4270,9 @@ var MockTransport = class {
|
|
|
3980
4270
|
async getRules(params) {
|
|
3981
4271
|
return this.backend.getRules(params);
|
|
3982
4272
|
}
|
|
4273
|
+
async learnReviewRule(reviewId, req) {
|
|
4274
|
+
return this.backend.learnReviewRule(reviewId, req);
|
|
4275
|
+
}
|
|
3983
4276
|
async saveRule(req) {
|
|
3984
4277
|
return this.backend.saveRule(req);
|
|
3985
4278
|
}
|
|
@@ -4029,6 +4322,49 @@ function filenameFromDisposition(disposition) {
|
|
|
4029
4322
|
return m ? decodeURIComponent(m[1].trim()) : "";
|
|
4030
4323
|
}
|
|
4031
4324
|
|
|
4325
|
+
// src/domain-wait.ts
|
|
4326
|
+
async function waitForDomain(get, options = {}) {
|
|
4327
|
+
const seconds = options.timeout_seconds ?? 45;
|
|
4328
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 50) throw new Error("timeout_seconds must be an integer between 0 and 50");
|
|
4329
|
+
const deadline = AbortSignal.timeout(Math.max(1, seconds * 1e3));
|
|
4330
|
+
const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
4331
|
+
let latest;
|
|
4332
|
+
const result = (outcome) => ({
|
|
4333
|
+
domain: latest,
|
|
4334
|
+
outcome,
|
|
4335
|
+
resume_after_seconds: outcome === "timed_out" ? Math.max(5, Math.min(60, latest?.readiness?.poll_after_seconds ?? 30)) : 0
|
|
4336
|
+
});
|
|
4337
|
+
try {
|
|
4338
|
+
const singleCheckDeadline = seconds === 0 ? AbortSignal.timeout(1e4) : void 0;
|
|
4339
|
+
latest = await get(singleCheckDeadline ? options.signal ? AbortSignal.any([options.signal, singleCheckDeadline]) : singleCheckDeadline : signal);
|
|
4340
|
+
for (; ; ) {
|
|
4341
|
+
const r = latest.readiness;
|
|
4342
|
+
if (!r) return result("status_unavailable");
|
|
4343
|
+
if (r.ready_for_inboxes) return result("ready");
|
|
4344
|
+
if (r.action_required_by === "customer") return result("action_required");
|
|
4345
|
+
if (r.status === "needs_attention") return result("needs_attention");
|
|
4346
|
+
if (seconds === 0) return result("timed_out");
|
|
4347
|
+
await new Promise((resolve, reject) => {
|
|
4348
|
+
signal.throwIfAborted();
|
|
4349
|
+
const abort = () => {
|
|
4350
|
+
clearTimeout(timer);
|
|
4351
|
+
reject(signal.reason);
|
|
4352
|
+
};
|
|
4353
|
+
const timer = setTimeout(() => {
|
|
4354
|
+
signal.removeEventListener("abort", abort);
|
|
4355
|
+
resolve();
|
|
4356
|
+
}, Math.max(5, Math.min(60, r.poll_after_seconds || 30)) * 1e3);
|
|
4357
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4358
|
+
});
|
|
4359
|
+
latest = await get(signal);
|
|
4360
|
+
}
|
|
4361
|
+
} catch (error) {
|
|
4362
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
4363
|
+
if (deadline.aborted && latest) return result("timed_out");
|
|
4364
|
+
throw error;
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4032
4368
|
// src/key-tier.ts
|
|
4033
4369
|
var AGENT_HEAD = "pk_agent_";
|
|
4034
4370
|
function parseKeyTier(apiKey) {
|
|
@@ -4113,8 +4449,8 @@ var InboxHandle = class {
|
|
|
4113
4449
|
* Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
|
|
4114
4450
|
* human has to approve it and NOTHING has been delivered yet; anything else was
|
|
4115
4451
|
* delivered. Under the default `require_review` policy a call WITHOUT an
|
|
4116
|
-
* `intent` raises `IntentRequiredError` (422) instead
|
|
4117
|
-
* queued
|
|
4452
|
+
* `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
|
|
4453
|
+
* queued: so pass one, or read `inbox.record.effective_review_policy` first.
|
|
4118
4454
|
*/
|
|
4119
4455
|
send(req, signal) {
|
|
4120
4456
|
return this.transport.send(this.ref, req, signal);
|
|
@@ -4124,7 +4460,7 @@ var InboxHandle = class {
|
|
|
4124
4460
|
* the latest message) or `message_id` (reply to that message); the server
|
|
4125
4461
|
* derives To / Subject / In-Reply-To / References. Set `reply_all` to reply to
|
|
4126
4462
|
* every thread recipient. Returns the same three-way {@link SendOutcome} as
|
|
4127
|
-
* {@link send}
|
|
4463
|
+
* {@link send}: a reply is governed by the review policy too.
|
|
4128
4464
|
*/
|
|
4129
4465
|
reply(req, signal) {
|
|
4130
4466
|
return this.transport.reply(this.ref, req, signal);
|
|
@@ -4133,7 +4469,7 @@ var InboxHandle = class {
|
|
|
4133
4469
|
* Forward a message in this inbox to new recipients, preserving the original.
|
|
4134
4470
|
*
|
|
4135
4471
|
* A forward is an outbound message to arbitrary NEW recipients that quotes an
|
|
4136
|
-
* inbound thread, so it is governed by the review policy exactly like a send
|
|
4472
|
+
* inbound thread, so it is governed by the review policy exactly like a send :
|
|
4137
4473
|
* same {@link SendOutcome} union, same `intent` requirement.
|
|
4138
4474
|
*/
|
|
4139
4475
|
forward(messageId, req, signal) {
|
|
@@ -4297,7 +4633,7 @@ var ListPage = class _ListPage {
|
|
|
4297
4633
|
this.nextCursor = raw.next_cursor;
|
|
4298
4634
|
}
|
|
4299
4635
|
/**
|
|
4300
|
-
* Fetch the next page. Throws if there is none
|
|
4636
|
+
* Fetch the next page. Throws if there is none - guard with {@link hasMore}.
|
|
4301
4637
|
*/
|
|
4302
4638
|
async nextPage(signal) {
|
|
4303
4639
|
if (!this.hasMore || this.nextCursor === null) {
|
|
@@ -4385,8 +4721,8 @@ var ProjectInboxes = class {
|
|
|
4385
4721
|
* Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
|
|
4386
4722
|
* human has to approve it and NOTHING has been delivered yet; anything else was
|
|
4387
4723
|
* delivered. Under the default `require_review` policy a call WITHOUT an
|
|
4388
|
-
* `intent` raises `IntentRequiredError` (422) instead
|
|
4389
|
-
* queued
|
|
4724
|
+
* `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
|
|
4725
|
+
* queued: so pass one, or read `inbox.record.effective_review_policy` first.
|
|
4390
4726
|
*/
|
|
4391
4727
|
send(projectId, inboxId, req, signal) {
|
|
4392
4728
|
return this.ctx.transport.send(this.ref(projectId, inboxId), req, signal);
|
|
@@ -4452,14 +4788,14 @@ var ProjectInboxes = class {
|
|
|
4452
4788
|
*
|
|
4453
4789
|
* The frozen contract project-prefixes ONLY the inbox collection/item/credentials
|
|
4454
4790
|
* routes (`/v1/projects/{project_id}/inboxes[/{inbox_id}][/credentials]`); the
|
|
4455
|
-
* send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path
|
|
4791
|
+
* send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path :
|
|
4456
4792
|
* they address the inbox by its opaque id directly (`/v1/inboxes/{inbox_id}/…`),
|
|
4457
4793
|
* where the project is implicit in (and enforced by) the inbox id server-side.
|
|
4458
4794
|
*
|
|
4459
4795
|
* So for these sub-ops `projectId` cannot be carried on the URL and is NOT a URL
|
|
4460
4796
|
* selector. The adversarial review flagged that silently discarding it makes the
|
|
4461
4797
|
* signature misleading. CHOICE: keep the arg (dropping it would break the chain's
|
|
4462
|
-
* symmetry with create/list/get/update/delete
|
|
4798
|
+
* symmetry with create/list/get/update/delete: the more disruptive option) but
|
|
4463
4799
|
* VALIDATE it rather than ignore it. We reject the two client mistakes we can catch
|
|
4464
4800
|
* without a round-trip:
|
|
4465
4801
|
* - a blank / whitespace-only `projectId` (a required selector everywhere else in
|
|
@@ -4498,8 +4834,9 @@ var Inboxes = class {
|
|
|
4498
4834
|
this.ctx = ctx;
|
|
4499
4835
|
}
|
|
4500
4836
|
/**
|
|
4501
|
-
* Create an inbox. The default path
|
|
4502
|
-
* `
|
|
4837
|
+
* Create an inbox. The default path creates an address on `extrovertmail.com`
|
|
4838
|
+
* for paid accounts or `free.extrovertmail.com` for free signups, so it returns
|
|
4839
|
+
* a live inbox in one call.
|
|
4503
4840
|
*
|
|
4504
4841
|
* Pass `metadata` to attach arbitrary key-value data, and `client_id` for idempotent creation
|
|
4505
4842
|
* (re-calling with the same id returns the same inbox, with its metadata replayed verbatim).
|
|
@@ -4509,12 +4846,12 @@ var Inboxes = class {
|
|
|
4509
4846
|
return new InboxHandle(this.ctx.transport, inbox.address, this.ctx.handleOptions, inbox);
|
|
4510
4847
|
}
|
|
4511
4848
|
/**
|
|
4512
|
-
* List inboxes visible to the calling key (the bare curl-sugar surface
|
|
4849
|
+
* List inboxes visible to the calling key (the bare curl-sugar surface: resolves
|
|
4513
4850
|
* to the key's default project). An org-tier key has no single default project, so
|
|
4514
4851
|
* the bare list is ambiguous: fail fast client-side with a BreadthRequiredError that
|
|
4515
4852
|
* names the next call, matching the MCP surface, instead of round-tripping to a 400.
|
|
4516
4853
|
* Use `extrovert.projects.inboxes.list("<project_id>")` or `"-"` (org subtree) for
|
|
4517
|
-
* an org key. The check is advisory
|
|
4854
|
+
* an org key. The check is advisory: the server stays authoritative.
|
|
4518
4855
|
*/
|
|
4519
4856
|
list(params = {}, signal) {
|
|
4520
4857
|
if (tierNeedsExplicitBreadth(this.ctx.keyTier)) {
|
|
@@ -4610,10 +4947,22 @@ var Threads = class {
|
|
|
4610
4947
|
constructor(ctx) {
|
|
4611
4948
|
this.ctx = ctx;
|
|
4612
4949
|
}
|
|
4950
|
+
/** List conversations newest-active first. Pass `next_cursor` back as `cursor` for the next page. */
|
|
4951
|
+
list(inbox, params = {}, signal) {
|
|
4952
|
+
return this.ctx.transport.listThreads(inbox, params, signal);
|
|
4953
|
+
}
|
|
4954
|
+
/** Search thread subjects, snippets, and participants. Cursor pagination matches {@link list}. */
|
|
4955
|
+
search(inbox, params, signal) {
|
|
4956
|
+
return this.ctx.transport.searchThreads(inbox, params, signal);
|
|
4957
|
+
}
|
|
4613
4958
|
/** Fetch one thread (+ its messages, oldest-first) by id under its owning inbox address. */
|
|
4614
4959
|
get(inbox, threadId, signal) {
|
|
4615
4960
|
return this.ctx.transport.getThread(inbox, threadId, signal);
|
|
4616
4961
|
}
|
|
4962
|
+
/** Reply in a thread; recipients and RFC reply headers are derived server-side. */
|
|
4963
|
+
reply(inbox, req, signal) {
|
|
4964
|
+
return this.ctx.transport.reply(inbox, req, signal);
|
|
4965
|
+
}
|
|
4617
4966
|
/**
|
|
4618
4967
|
* Delete an entire thread (every message): move to Trash (default) or
|
|
4619
4968
|
* permanently remove when `expunge` is true. `inbox` is the owning address.
|
|
@@ -4675,7 +5024,7 @@ var Suppressions = class {
|
|
|
4675
5024
|
}
|
|
4676
5025
|
/**
|
|
4677
5026
|
* Pre-check whether the caller's org already suppresses a recipient, BEFORE
|
|
4678
|
-
* composing. `suppressed: true` means a send to them would be rejected
|
|
5027
|
+
* composing. `suppressed: true` means a send to them would be rejected: skip
|
|
4679
5028
|
* that recipient. Returns the matching org rows too (never a global/shared row).
|
|
4680
5029
|
*/
|
|
4681
5030
|
precheck(recipient, signal) {
|
|
@@ -4699,17 +5048,25 @@ var Domains = class {
|
|
|
4699
5048
|
this.ctx = ctx;
|
|
4700
5049
|
}
|
|
4701
5050
|
/** List the customer's onboarded domains and their status. */
|
|
4702
|
-
list(signal) {
|
|
4703
|
-
return this.ctx.transport.listDomains(
|
|
5051
|
+
list(paramsOrSignal = {}, signal) {
|
|
5052
|
+
if ("aborted" in paramsOrSignal) return this.ctx.transport.listDomains(paramsOrSignal);
|
|
5053
|
+
return this.ctx.transport.listDomains(signal, paramsOrSignal);
|
|
4704
5054
|
}
|
|
4705
|
-
/** Get one domain's detail
|
|
5055
|
+
/** Get one domain's detail, verification status, and nameserver records. */
|
|
4706
5056
|
get(domain, signal) {
|
|
4707
5057
|
return this.ctx.transport.getDomain(domain, signal);
|
|
4708
5058
|
}
|
|
5059
|
+
/** Wait up to 50 seconds, then return an explicit resumable outcome. No DNS writes. */
|
|
5060
|
+
wait(domain, options = {}) {
|
|
5061
|
+
return waitForDomain((signal) => this.ctx.transport.getDomain(domain, signal), options);
|
|
5062
|
+
}
|
|
5063
|
+
/** Resume durable updates for this domain using the previous next_cursor as after. */
|
|
5064
|
+
events(domain, params = {}, signal) {
|
|
5065
|
+
return this.ctx.transport.listDomainEvents(domain, params, signal);
|
|
5066
|
+
}
|
|
4709
5067
|
/**
|
|
4710
|
-
*
|
|
4711
|
-
*
|
|
4712
|
-
* the record set / NS instruction.
|
|
5068
|
+
* Add a delegated inbox domain the customer controls. Returns the nameserver
|
|
5069
|
+
* records to publish and never spends money.
|
|
4713
5070
|
*/
|
|
4714
5071
|
onboard(req, signal) {
|
|
4715
5072
|
return this.ctx.transport.onboardDomain(req, signal);
|
|
@@ -4728,6 +5085,46 @@ var Domains = class {
|
|
|
4728
5085
|
return this.ctx.transport.offboardDomain(domain, signal);
|
|
4729
5086
|
}
|
|
4730
5087
|
};
|
|
5088
|
+
var Commerce = class {
|
|
5089
|
+
constructor(ctx) {
|
|
5090
|
+
this.ctx = ctx;
|
|
5091
|
+
}
|
|
5092
|
+
requireIdempotencyKey(value) {
|
|
5093
|
+
if (value.trim().length < 8) {
|
|
5094
|
+
throw new ValidationError({
|
|
5095
|
+
status: 400,
|
|
5096
|
+
code: "bad_request",
|
|
5097
|
+
message: "idempotency_key must be a stable value of at least 8 characters; reuse it for retries of the same intent."
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
/** Quote a domain without purchasing, reserving, or approving it. */
|
|
5102
|
+
quoteDomain(req, signal) {
|
|
5103
|
+
return this.ctx.transport.quoteDomain(req, signal);
|
|
5104
|
+
}
|
|
5105
|
+
/** Create a durable domain-purchase request for human approval. */
|
|
5106
|
+
requestDomainPurchase(req, signal) {
|
|
5107
|
+
this.requireIdempotencyKey(req.idempotency_key);
|
|
5108
|
+
return this.ctx.transport.requestDomainPurchase(req, signal);
|
|
5109
|
+
}
|
|
5110
|
+
/** Create a durable plan-upgrade or downgrade request for human approval. */
|
|
5111
|
+
requestPlanChange(req, signal) {
|
|
5112
|
+
this.requireIdempotencyKey(req.idempotency_key);
|
|
5113
|
+
return this.ctx.transport.requestPlanChange(req, signal);
|
|
5114
|
+
}
|
|
5115
|
+
/** Poll one request's exact blockers, approval URL, and next-action guidance. */
|
|
5116
|
+
get(requestId, signal) {
|
|
5117
|
+
return this.ctx.transport.getCommerceRequest(requestId, signal);
|
|
5118
|
+
}
|
|
5119
|
+
/** Withdraw this agent's request while its durable state still permits cancellation. */
|
|
5120
|
+
cancel(requestId, signal) {
|
|
5121
|
+
return this.ctx.transport.cancelCommerceRequest(requestId, signal);
|
|
5122
|
+
}
|
|
5123
|
+
/** List visible commerce requests using the API's opaque page token. */
|
|
5124
|
+
list(params = {}, signal) {
|
|
5125
|
+
return this.ctx.transport.listCommerceRequests(params, signal);
|
|
5126
|
+
}
|
|
5127
|
+
};
|
|
4731
5128
|
var Reviews = class {
|
|
4732
5129
|
constructor(ctx) {
|
|
4733
5130
|
this.ctx = ctx;
|
|
@@ -4748,7 +5145,7 @@ var Reviews = class {
|
|
|
4748
5145
|
/**
|
|
4749
5146
|
* Get the human's assembled feedback (M5): the diff + comments + decision + the
|
|
4750
5147
|
* rules born from this review. Read it after a rejected/edited nudge to learn what
|
|
4751
|
-
* the human wanted. $0 LLM
|
|
5148
|
+
* the human wanted. $0 LLM: pure assembly on our side.
|
|
4752
5149
|
*/
|
|
4753
5150
|
feedback(reviewId, signal) {
|
|
4754
5151
|
return this.ctx.transport.getReviewFeedback(reviewId, signal);
|
|
@@ -4756,7 +5153,7 @@ var Reviews = class {
|
|
|
4756
5153
|
/**
|
|
4757
5154
|
* Post a chat turn on a review's thread (M5): an agent question to the human
|
|
4758
5155
|
* reviewer; flips in_review -> chatting on the first turn. Idempotent on the
|
|
4759
|
-
* optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM
|
|
5156
|
+
* optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM: you compose it.
|
|
4760
5157
|
*/
|
|
4761
5158
|
chat(reviewId, req, idempotencyKey, signal) {
|
|
4762
5159
|
return this.ctx.transport.postReviewChat(reviewId, req, idempotencyKey, signal);
|
|
@@ -4764,8 +5161,8 @@ var Reviews = class {
|
|
|
4764
5161
|
/**
|
|
4765
5162
|
* Post a new agent draft under a parent_revision CAS (M5; D17). parent_revision
|
|
4766
5163
|
* must equal the draft's current revision, else a 409 STALE with NO mutation (the
|
|
4767
|
-
* human always wins
|
|
4768
|
-
* in place (revision++) and returns to needs_review. $0 LLM
|
|
5164
|
+
* human always wins: re-read, re-apply, retry). On success the draft is re-rendered
|
|
5165
|
+
* in place (revision++) and returns to needs_review. $0 LLM: you compose the redraft.
|
|
4769
5166
|
*/
|
|
4770
5167
|
revise(reviewId, req, signal) {
|
|
4771
5168
|
return this.ctx.transport.submitRevision(reviewId, req, signal);
|
|
@@ -4784,9 +5181,9 @@ var Reviews = class {
|
|
|
4784
5181
|
* assert "I reviewed this against rules vX and no change is needed", advancing the
|
|
4785
5182
|
* draft's composed_* versions with no new draft, no revision bump, no nudge. A
|
|
4786
5183
|
* born-stale draft re-stamped to the current version becomes current-enough and
|
|
4787
|
-
* releasable on the next reconciliation sweep
|
|
5184
|
+
* releasable on the next reconciliation sweep: the cheap counterpart to revise().
|
|
4788
5185
|
* against_version above the category's current rules-version is 400; a terminal draft
|
|
4789
|
-
* 409s. $0 LLM
|
|
5186
|
+
* 409s. $0 LLM: you judged.
|
|
4790
5187
|
*/
|
|
4791
5188
|
restamp(reviewId, req, signal) {
|
|
4792
5189
|
return this.ctx.transport.restampReview(reviewId, req, signal);
|
|
@@ -4805,13 +5202,13 @@ var Reviews = class {
|
|
|
4805
5202
|
}
|
|
4806
5203
|
/**
|
|
4807
5204
|
* Submit a reviewer decision (M8 Slice B; reviewer_decide, D5/§9). approve/edit → the
|
|
4808
|
-
* PLATFORM
|
|
4809
|
-
* mailbox:send on an inbox it doesn't own
|
|
5205
|
+
* PLATFORM sends with the COMPOSER's credentials (the reviewer NEVER holds
|
|
5206
|
+
* mailbox:send on an inbox it doesn't own: the credential boundary); reject → back to
|
|
4810
5207
|
* the composer (needs_review, hop_count++); escalate → the human queue. revision/
|
|
4811
|
-
* version are the CAS (409 STALE on mismatch, NO mutation
|
|
5208
|
+
* version are the CAS (409 STALE on mismatch, NO mutation: the human always wins,
|
|
4812
5209
|
* D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline)
|
|
4813
|
-
* FORCE a reject to the human regardless of intent
|
|
4814
|
-
* LLM
|
|
5210
|
+
* FORCE a reject to the human regardless of intent: `forced_by_breaker` names it. $0
|
|
5211
|
+
* LLM: you judged; we route, send, and enforce the breakers.
|
|
4815
5212
|
*/
|
|
4816
5213
|
decide(reviewId, req, signal) {
|
|
4817
5214
|
return this.ctx.transport.reviewerDecide(reviewId, req, signal);
|
|
@@ -4850,14 +5247,14 @@ var Categories = class {
|
|
|
4850
5247
|
propose(req, signal) {
|
|
4851
5248
|
return this.ctx.transport.proposeCategory(req, signal);
|
|
4852
5249
|
}
|
|
4853
|
-
/** Rename / re-describe a category
|
|
5250
|
+
/** Rename / re-describe a category: metadata only (D10). */
|
|
4854
5251
|
update(categoryId, req, signal) {
|
|
4855
5252
|
return this.ctx.transport.updateCategory(categoryId, req, signal);
|
|
4856
5253
|
}
|
|
4857
5254
|
/**
|
|
4858
5255
|
* Read the effective risk dial (D4/D12): the account default + every category's
|
|
4859
5256
|
* overrides (each with its resolved effective value; null override = inherit).
|
|
4860
|
-
* Read-only
|
|
5257
|
+
* Read-only: agents read but NEVER flip the dial; setting it is a human (console)
|
|
4861
5258
|
* action (D16).
|
|
4862
5259
|
*/
|
|
4863
5260
|
riskDial(signal) {
|
|
@@ -4873,7 +5270,7 @@ var Categories = class {
|
|
|
4873
5270
|
}
|
|
4874
5271
|
/**
|
|
4875
5272
|
* Propose graduating a category (D16/D6): RECORDS the request (durable evidence) and
|
|
4876
|
-
* returns the current gate status. It does NOT change the category state
|
|
5273
|
+
* returns the current gate status. It does NOT change the category state: flipping
|
|
4877
5274
|
* the bit is a human (console) action; an agent only proposes.
|
|
4878
5275
|
*/
|
|
4879
5276
|
proposeGraduation(categoryId, req = {}, signal) {
|
|
@@ -4882,7 +5279,7 @@ var Categories = class {
|
|
|
4882
5279
|
/**
|
|
4883
5280
|
* Read the D19/§8 backlog-reconciliation status: how many of the category's QUEUED
|
|
4884
5281
|
* drafts are stale vs current-enough against the current rules-version (a pure
|
|
4885
|
-
* integer compare, $0 LLM). Read-only
|
|
5282
|
+
* integer compare, $0 LLM). Read-only: you READ the picture; the human (console
|
|
4886
5283
|
* scan-backlog) or the graduate/rule-change hooks TRIGGER the actual reconciliation
|
|
4887
5284
|
* sweep that releases current-enough drafts and nudges stale ones to redraft.
|
|
4888
5285
|
*/
|
|
@@ -4905,6 +5302,10 @@ var Rules = class {
|
|
|
4905
5302
|
constructor(ctx) {
|
|
4906
5303
|
this.ctx = ctx;
|
|
4907
5304
|
}
|
|
5305
|
+
/** Learn category or organization house rules from verified human review feedback. */
|
|
5306
|
+
learnFromReview(reviewId, req, signal) {
|
|
5307
|
+
return this.ctx.transport.learnReviewRule(reviewId, req, signal);
|
|
5308
|
+
}
|
|
4908
5309
|
/** Get the ORDERED active rule set (precedence ladder applied; NO LLM). */
|
|
4909
5310
|
get(params = {}, signal) {
|
|
4910
5311
|
return this.ctx.transport.getRules(params, signal);
|
|
@@ -4912,8 +5313,8 @@ var Rules = class {
|
|
|
4912
5313
|
/**
|
|
4913
5314
|
* Save / edit a rule (append-only by supersession; D11). An agent-plane save is
|
|
4914
5315
|
* ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
|
|
4915
|
-
* key's project.
|
|
4916
|
-
* rules in v1
|
|
5316
|
+
* key's project. For org-layer house rules use learnFromReview with an authenticated human source; this method cannot author (`rule_layer:"org"`)
|
|
5317
|
+
* rules in v1: that is a console/admin action.
|
|
4917
5318
|
*/
|
|
4918
5319
|
save(req, signal) {
|
|
4919
5320
|
return this.ctx.transport.saveRule(req, signal);
|
|
@@ -4922,7 +5323,7 @@ var Rules = class {
|
|
|
4922
5323
|
promote(ruleId, toScope, signal) {
|
|
4923
5324
|
return this.ctx.transport.promoteRule(ruleId, toScope, signal);
|
|
4924
5325
|
}
|
|
4925
|
-
/** Retire a rule
|
|
5326
|
+
/** Retire a rule: soft delete; the history survives as training data. */
|
|
4926
5327
|
retire(ruleId, signal) {
|
|
4927
5328
|
return this.ctx.transport.retireRule(ruleId, signal);
|
|
4928
5329
|
}
|
|
@@ -4930,7 +5331,7 @@ var Rules = class {
|
|
|
4930
5331
|
audit(params = {}, signal) {
|
|
4931
5332
|
return this.ctx.transport.getRuleAudit(params, signal);
|
|
4932
5333
|
}
|
|
4933
|
-
/** Undo a rule change by its audit-row id (udo_…)
|
|
5334
|
+
/** Undo a rule change by its audit-row id (udo_…): restore the prior version. */
|
|
4934
5335
|
undo(udoId, signal) {
|
|
4935
5336
|
return this.ctx.transport.undoRuleChange(udoId, signal);
|
|
4936
5337
|
}
|
|
@@ -4987,26 +5388,29 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
4987
5388
|
this.contactLists = new ContactLists(ctx);
|
|
4988
5389
|
this.suppressions = new Suppressions(ctx);
|
|
4989
5390
|
this.domains = new Domains(ctx);
|
|
5391
|
+
this.commerce = new Commerce(ctx);
|
|
4990
5392
|
this.reviews = new Reviews(ctx);
|
|
4991
5393
|
this.categories = new Categories(ctx);
|
|
4992
5394
|
this.rules = new Rules(ctx);
|
|
4993
5395
|
this.projects = new Projects(ctx);
|
|
4994
5396
|
}
|
|
4995
5397
|
/**
|
|
4996
|
-
* Redeem an enrollment token (`pk_enroll_...`) and
|
|
5398
|
+
* Redeem an enrollment token (`pk_enroll_...`) and issue a scoped agent key.
|
|
4997
5399
|
*
|
|
4998
5400
|
* Idempotent on `agent_handle`: redeeming twice with the same handle returns the same agent.
|
|
4999
|
-
* Returns the raw `EnrollResponse`
|
|
5401
|
+
* Returns the raw `EnrollResponse` - to immediately use the issued key, prefer
|
|
5000
5402
|
* {@link ExtrovertClient.enrolled}.
|
|
5001
5403
|
*/
|
|
5002
5404
|
enroll(req, signal) {
|
|
5003
5405
|
return this.transport.enroll(req, signal);
|
|
5004
5406
|
}
|
|
5005
5407
|
/**
|
|
5006
|
-
*
|
|
5007
|
-
*
|
|
5008
|
-
*
|
|
5009
|
-
*
|
|
5408
|
+
* Request a free account in one unauthenticated call. When free signup is
|
|
5409
|
+
* enabled, this provisions a tenant plus a first inbox and returns a
|
|
5410
|
+
* verification-only agent key. That key can only call {@link verify}; it cannot
|
|
5411
|
+
* read or send mail. A one-time code is emailed to `human_email`. Call
|
|
5412
|
+
* {@link verify} with the code to activate the account and receive full scopes.
|
|
5413
|
+
* Idempotent on `human_email`: re-calling rotates the key and resends the code.
|
|
5010
5414
|
* When free signup is paused, this throws an `ApiError` with status 403 and
|
|
5011
5415
|
* code `signup_disabled` without creating account state.
|
|
5012
5416
|
*/
|
|
@@ -5029,7 +5433,7 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5029
5433
|
return this.transport.whoami(signal);
|
|
5030
5434
|
}
|
|
5031
5435
|
/**
|
|
5032
|
-
* Poll the status of an async job (`GET /v1/jobs/{job_id}`)
|
|
5436
|
+
* Poll the status of an async job (`GET /v1/jobs/{job_id}`) - currently only
|
|
5033
5437
|
* the domain-offboard teardown started by {@link Domains.offboard} enqueues
|
|
5034
5438
|
* one. `status` is terminal on succeeded/failed/cancelled; keep polling
|
|
5035
5439
|
* otherwise. An unknown or foreign job id is a {@link NotFoundError}.
|
|
@@ -5038,8 +5442,8 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5038
5442
|
return this.transport.getJob(jobId, signal);
|
|
5039
5443
|
}
|
|
5040
5444
|
/**
|
|
5041
|
-
* Redeem an enrollment token and return a *new* client already authenticated with the
|
|
5042
|
-
* agent key
|
|
5445
|
+
* Redeem an enrollment token and return a *new* client already authenticated with the issued
|
|
5446
|
+
* agent key - the natural "redeem then act" flow for an agent.
|
|
5043
5447
|
*
|
|
5044
5448
|
* ```ts
|
|
5045
5449
|
* const bootstrap = new Extrovert({ apiKey: enrollmentToken });
|
|
@@ -5060,7 +5464,7 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5060
5464
|
return { client, enrollment };
|
|
5061
5465
|
}
|
|
5062
5466
|
/**
|
|
5063
|
-
* Get an ergonomic handle to an existing inbox by address
|
|
5467
|
+
* Get an ergonomic handle to an existing inbox by address - without an extra round-trip. Use this
|
|
5064
5468
|
* when you already know the address (e.g. from a previous create) and want to send/wait/reply.
|
|
5065
5469
|
* Call {@link InboxHandle.refresh} to load the full record.
|
|
5066
5470
|
*/
|
|
@@ -5187,14 +5591,14 @@ async function signWebhook(secret, body, timestampSeconds) {
|
|
|
5187
5591
|
}
|
|
5188
5592
|
|
|
5189
5593
|
// src/contract.ts
|
|
5190
|
-
var CONTRACT_VERSION = "0.1.0-pre.
|
|
5594
|
+
var CONTRACT_VERSION = "0.1.0-pre.8";
|
|
5191
5595
|
var CONTRACT_MANIFEST = {
|
|
5192
5596
|
name: "extrovert.review-loop",
|
|
5193
5597
|
version: CONTRACT_VERSION,
|
|
5194
5598
|
stability: "provisional",
|
|
5195
5599
|
kind: "sdk+skill-contract",
|
|
5196
5600
|
spec_ref: "hitl-spec.md#11",
|
|
5197
|
-
// §11 core
|
|
5601
|
+
// §11 core - the five canonical example shapes.
|
|
5198
5602
|
core_shapes: ["ReviewIntent", "ReviewFeedback", "DiffJson", "Rule", "ReviewEvent"],
|
|
5199
5603
|
// The FULL published surface (the §11 core plus the rest of M1–M8). Adding a
|
|
5200
5604
|
// name here without a matching re-export (or vice-versa) breaks the drift test.
|
|
@@ -5218,6 +5622,8 @@ var CONTRACT_MANIFEST = {
|
|
|
5218
5622
|
// realtime (M3)
|
|
5219
5623
|
"ReviewEventReason",
|
|
5220
5624
|
"ReviewEventsResult",
|
|
5625
|
+
"LearnReviewRuleRequest",
|
|
5626
|
+
"LearnedReviewRule",
|
|
5221
5627
|
"ReviewEventCursor",
|
|
5222
5628
|
// chat / revision / restamp (M5/M7)
|
|
5223
5629
|
"PostReviewChatRequest",
|
|
@@ -5245,13 +5651,22 @@ var CONTRACT_MANIFEST = {
|
|
|
5245
5651
|
"ReviewerAction",
|
|
5246
5652
|
"ReviewDecisionContext",
|
|
5247
5653
|
"ReviewerDecisionRequest",
|
|
5248
|
-
"ReviewerDecisionResult"
|
|
5654
|
+
"ReviewerDecisionResult",
|
|
5655
|
+
// agent commerce request plane
|
|
5656
|
+
"CommerceBlocker",
|
|
5657
|
+
"QuoteDomainRequest",
|
|
5658
|
+
"DomainQuote",
|
|
5659
|
+
"CommerceRequestKind",
|
|
5660
|
+
"RequestDomainPurchaseRequest",
|
|
5661
|
+
"RequestPlanChangeRequest",
|
|
5662
|
+
"ListCommerceRequestsParams",
|
|
5663
|
+
"CommerceRequest"
|
|
5249
5664
|
],
|
|
5250
5665
|
// The complete Review Loop behavior lives in the send skill; writing-rule
|
|
5251
5666
|
// governance remains independently installable and part of this contract.
|
|
5252
5667
|
skills: ["extrovert-send-email", "extrovert-writing-rules"]
|
|
5253
5668
|
};
|
|
5254
5669
|
|
|
5255
|
-
export { API_VERSION_HEADER, ApiError, AuthenticationError, BornStaleError, BreadthRequiredError, CONTRACT_MANIFEST, CONTRACT_VERSION, CURRENT_API_VERSION, Categories, ConflictError, ConnectionError, ContactLists, DEFAULT_BASE_URL, Domains, ExtrovertClient as Extrovert, ExtrovertClient, ForbiddenScopeError, IdempotencyConflictError, InboxHandle, Inboxes, IntentRequiredError, ListPage, MOCK_BASE_URL, Messages, MockBackend, NotFoundError, PROBLEM_CODES, PaymentRequiredError, PermissionError, ProjectInboxes, Projects, REVIEW_PROBLEM_RETRYABLE, RateLimitError, RecipientSuppressedError, ReviewConflictError, ReviewEvents, Reviews, Rules, SDK_VERSION, SendNeedsReconciliationError, StaleError, Suppressions, TerminalError, Threads, TimeoutError, UnavailableError, ValidationError, Webhooks, WrongStateError, extractCredentials, extractLink, extractOtp, isProblemCode, isQueuedForReview, isSentImmediately, listPage, parseKeyTier, parseProblem, parseWebhook, reviewIdOf, sentMessageIdOf, serializeInclude, signWebhook, threadIdOf, tierAllowsOrgWildcard, tierNeedsExplicitBreadth, verifyWebhookSignature };
|
|
5670
|
+
export { API_VERSION_HEADER, ApiError, AuthenticationError, BornStaleError, BreadthRequiredError, CONTRACT_MANIFEST, CONTRACT_VERSION, CURRENT_API_VERSION, Categories, Commerce, ConflictError, ConnectionError, ContactLists, DEFAULT_BASE_URL, Domains, ExtrovertClient as Extrovert, ExtrovertClient, ForbiddenScopeError, IdempotencyConflictError, InboxHandle, Inboxes, IntentRequiredError, ListPage, MOCK_BASE_URL, Messages, MockBackend, NotFoundError, PROBLEM_CODES, PaymentRequiredError, PermissionError, ProjectInboxes, Projects, REVIEW_PROBLEM_RETRYABLE, RateLimitError, RecipientSuppressedError, ReviewConflictError, ReviewEvents, Reviews, Rules, SDK_VERSION, SendNeedsReconciliationError, StaleError, Suppressions, TerminalError, Threads, TimeoutError, UnavailableError, ValidationError, Webhooks, WrongStateError, extractCredentials, extractLink, extractOtp, isProblemCode, isQueuedForReview, isSentImmediately, listPage, parseKeyTier, parseProblem, parseWebhook, reviewIdOf, sentMessageIdOf, serializeInclude, signWebhook, threadIdOf, tierAllowsOrgWildcard, tierNeedsExplicitBreadth, verifyWebhookSignature };
|
|
5256
5671
|
//# sourceMappingURL=index.js.map
|
|
5257
5672
|
//# sourceMappingURL=index.js.map
|