@extrovert.dev/sdk 0.1.0-pre.5 → 0.1.0-pre.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -65
- package/dist/index.cjs +514 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +544 -280
- package/dist/index.d.ts +544 -280
- package/dist/index.js +514 -118
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/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.7";
|
|
272
272
|
function buildUrl(baseUrl, path, query) {
|
|
273
273
|
const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
274
274
|
const rel = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -642,7 +642,23 @@ function mailboxQuickstart(address) {
|
|
|
642
642
|
}
|
|
643
643
|
};
|
|
644
644
|
}
|
|
645
|
-
var
|
|
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
|
}
|
|
@@ -936,7 +961,7 @@ var MockBackend = class {
|
|
|
936
961
|
agent_id: agentId,
|
|
937
962
|
agent_key: `pk_agent_proj_${agentId.slice(4)}_${rid("sk").slice(3)}`,
|
|
938
963
|
scopes: ["mailbox:create", "mailbox:read", "mailbox:send", "webhook:write"],
|
|
939
|
-
// The
|
|
964
|
+
// The issued key is bound to the token's resolved org/project; the agent cannot change it.
|
|
940
965
|
org_id: MOCK_ORG_ID,
|
|
941
966
|
project_id: MOCK_PROJECT_ID
|
|
942
967
|
};
|
|
@@ -951,7 +976,7 @@ var MockBackend = class {
|
|
|
951
976
|
const existing = this.state.signupByEmail.get(email);
|
|
952
977
|
const customerId = existing?.customerId ?? `cus_pn_signup_${rid("c").slice(2)}`;
|
|
953
978
|
const agentId = existing?.agentId ?? rid("agt");
|
|
954
|
-
const address = existing?.address ?? `${req.username ?? randomHandle()}
|
|
979
|
+
const address = existing?.address ?? `${validatedSharedLocalPart(req.username ?? randomHandle())}@${FREE_SHARED_DOMAIN}`;
|
|
955
980
|
const otp = "492013";
|
|
956
981
|
this.state.signupByEmail.set(email, { customerId, agentId, address, otp, verified: false });
|
|
957
982
|
return {
|
|
@@ -959,7 +984,7 @@ var MockBackend = class {
|
|
|
959
984
|
agent_id: agentId,
|
|
960
985
|
agent_key: `pk_agent_${agentId.slice(4)}_${rid("sk").slice(3)}`,
|
|
961
986
|
key_prefix: `pk_agent_${agentId.slice(4, 8)}`,
|
|
962
|
-
scopes: ["
|
|
987
|
+
scopes: ["signup:verify"],
|
|
963
988
|
address,
|
|
964
989
|
verified: false,
|
|
965
990
|
otp_sent_to: email,
|
|
@@ -1007,8 +1032,10 @@ var MockBackend = class {
|
|
|
1007
1032
|
}
|
|
1008
1033
|
}
|
|
1009
1034
|
const metadata = req.metadata ? mergeMetadata({}, req.metadata) : {};
|
|
1010
|
-
const
|
|
1011
|
-
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();
|
|
1012
1039
|
const id = rid("ibx");
|
|
1013
1040
|
const inbox = {
|
|
1014
1041
|
object: "inbox",
|
|
@@ -1023,6 +1050,7 @@ var MockBackend = class {
|
|
|
1023
1050
|
onboarding_mode: req.domain ? "ns_delegated" : "shared",
|
|
1024
1051
|
agent_id: null,
|
|
1025
1052
|
daily_send_limit: DEFAULT_DAILY_SEND_LIMIT,
|
|
1053
|
+
direct_smtp_enabled: false,
|
|
1026
1054
|
webhook_url: req.webhook_url ?? null,
|
|
1027
1055
|
metadata,
|
|
1028
1056
|
created_at: now(),
|
|
@@ -1061,7 +1089,7 @@ var MockBackend = class {
|
|
|
1061
1089
|
* Normalize an inbox ref (opaque id OR address alias) to the canonical address the
|
|
1062
1090
|
* mock keys its message/thread/contact maps on. The SDK now routes inbox ops by the
|
|
1063
1091
|
* canonical opaque `id` when it holds a full record (matching the contract's
|
|
1064
|
-
* canonical-key semantics), so the mock must resolve an id back to its address
|
|
1092
|
+
* canonical-key semantics), so the mock must resolve an id back to its address -
|
|
1065
1093
|
* both key `state.inboxes` (same object), `state.messages` keys by address only.
|
|
1066
1094
|
* Unknown refs pass through unchanged so the existing not-found paths still fire.
|
|
1067
1095
|
*/
|
|
@@ -1260,8 +1288,8 @@ var MockBackend = class {
|
|
|
1260
1288
|
}
|
|
1261
1289
|
/**
|
|
1262
1290
|
* Submit a new message for review (mock). Rides the SAME endpoint as `send` on
|
|
1263
|
-
* the real server, so it is literally the same call here: the resolved policy
|
|
1264
|
-
* not which SDK method you picked
|
|
1291
|
+
* the real server, so it is literally the same call here: the resolved policy -
|
|
1292
|
+
* not which SDK method you picked - decides whether the message is queued
|
|
1265
1293
|
* (`kind:"queued_for_review"`) or delivered.
|
|
1266
1294
|
*/
|
|
1267
1295
|
submitForReview(address, req) {
|
|
@@ -1343,7 +1371,7 @@ var MockBackend = class {
|
|
|
1343
1371
|
this.state.reviews.set(review.id, review);
|
|
1344
1372
|
this.enqueueTerminalNudge(review);
|
|
1345
1373
|
}
|
|
1346
|
-
/** Raw delivery for a send
|
|
1374
|
+
/** Raw delivery for a send - no policy, only reachable from submitOutbound. */
|
|
1347
1375
|
deliverSend(address, req) {
|
|
1348
1376
|
return this.deliverRaw(address, {
|
|
1349
1377
|
to: toArray(req.to),
|
|
@@ -1355,7 +1383,7 @@ var MockBackend = class {
|
|
|
1355
1383
|
attachments: req.attachments
|
|
1356
1384
|
});
|
|
1357
1385
|
}
|
|
1358
|
-
/** Raw delivery for a reply
|
|
1386
|
+
/** Raw delivery for a reply - no policy, only reachable from submitOutbound. */
|
|
1359
1387
|
deliverReply(address, req, env) {
|
|
1360
1388
|
return this.deliverRaw(address, {
|
|
1361
1389
|
to: env.to,
|
|
@@ -1393,6 +1421,13 @@ var MockBackend = class {
|
|
|
1393
1421
|
* the server does before it writes the review row.
|
|
1394
1422
|
*/
|
|
1395
1423
|
deriveReplyEnvelope(address, req) {
|
|
1424
|
+
if (Boolean(req.thread_id) === Boolean(req.message_id)) {
|
|
1425
|
+
throw new ValidationError({
|
|
1426
|
+
status: 400,
|
|
1427
|
+
code: "bad_request",
|
|
1428
|
+
message: "provide exactly one of thread_id or message_id"
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1396
1431
|
const all = this.state.messages.get(address) ?? [];
|
|
1397
1432
|
let parent;
|
|
1398
1433
|
let threadId = req.thread_id;
|
|
@@ -1410,6 +1445,13 @@ var MockBackend = class {
|
|
|
1410
1445
|
message: "thread_id or message_id is required"
|
|
1411
1446
|
});
|
|
1412
1447
|
}
|
|
1448
|
+
if (req.thread_id && req.expected_last_message_id && parent?.id !== req.expected_last_message_id) {
|
|
1449
|
+
throw new ConflictError({
|
|
1450
|
+
status: 409,
|
|
1451
|
+
code: "conflict",
|
|
1452
|
+
message: `thread advanced; latest message is ${parent?.id ?? "unknown"}`
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1413
1455
|
const to = [];
|
|
1414
1456
|
if (parent) {
|
|
1415
1457
|
to.push(parent.from.email);
|
|
@@ -1668,7 +1710,7 @@ var MockBackend = class {
|
|
|
1668
1710
|
* review id so a test can drive the reviewer decision plane offline. `createdAtMs`
|
|
1669
1711
|
* (optional) backdates created_at so a test can trip the hard review_deadline breaker.
|
|
1670
1712
|
*/
|
|
1671
|
-
seedReviewerHeldReview(opts = { fromAddress: "
|
|
1713
|
+
seedReviewerHeldReview(opts = { fromAddress: "reviewer@extrovertmail.com" }) {
|
|
1672
1714
|
const review = this.createReviewRecord(opts.fromAddress, {
|
|
1673
1715
|
kind: "send",
|
|
1674
1716
|
subject: "Pilot proposal",
|
|
@@ -1774,7 +1816,7 @@ var MockBackend = class {
|
|
|
1774
1816
|
// ---- Category registry (Review Loop, D9/D10) --------------------------
|
|
1775
1817
|
/**
|
|
1776
1818
|
* Browse the registry (mock), newest-first, excluding merged/soft-deleted. `match`
|
|
1777
|
-
* is a pure lexical filter (every token must appear in name+description)
|
|
1819
|
+
* is a pure lexical filter (every token must appear in name+description) - NO LLM,
|
|
1778
1820
|
* mirroring the server.
|
|
1779
1821
|
*/
|
|
1780
1822
|
listCategories(params = {}) {
|
|
@@ -1815,7 +1857,7 @@ var MockBackend = class {
|
|
|
1815
1857
|
this.state.categories.set(cat.id, cat);
|
|
1816
1858
|
return cat;
|
|
1817
1859
|
}
|
|
1818
|
-
/** Rename / re-describe a category (mock)
|
|
1860
|
+
/** Rename / re-describe a category (mock) - metadata only (D10). */
|
|
1819
1861
|
updateCategory(categoryId, req) {
|
|
1820
1862
|
const cat = this.state.categories.get(categoryId);
|
|
1821
1863
|
if (!cat) return void 0;
|
|
@@ -1825,7 +1867,7 @@ var MockBackend = class {
|
|
|
1825
1867
|
this.state.categories.set(cat.id, cat);
|
|
1826
1868
|
return cat;
|
|
1827
1869
|
}
|
|
1828
|
-
// ---- Graduation + risk dial (Review Loop, D16/D6/D17)
|
|
1870
|
+
// ---- Graduation + risk dial (Review Loop, D16/D6/D17) - agent READ + PROPOSE --
|
|
1829
1871
|
/** The mock account-default risk dial (mirrors the server defaults). */
|
|
1830
1872
|
accountDial() {
|
|
1831
1873
|
return {
|
|
@@ -1841,7 +1883,7 @@ var MockBackend = class {
|
|
|
1841
1883
|
/**
|
|
1842
1884
|
* Read the effective risk dial (mock): the account default + every category with an
|
|
1843
1885
|
* inherited (null override) effective dial. The mock category carries no overrides,
|
|
1844
|
-
* so every category inherits
|
|
1886
|
+
* so every category inherits - effective == account.
|
|
1845
1887
|
*/
|
|
1846
1888
|
getRiskDial() {
|
|
1847
1889
|
const account = this.accountDial();
|
|
@@ -1895,7 +1937,7 @@ var MockBackend = class {
|
|
|
1895
1937
|
}
|
|
1896
1938
|
/**
|
|
1897
1939
|
* Propose graduating a category (mock): returns the current gate status without
|
|
1898
|
-
* changing the category state (D16
|
|
1940
|
+
* changing the category state (D16 - an agent can never flip the bit).
|
|
1899
1941
|
*/
|
|
1900
1942
|
proposeGraduation(categoryId, _req) {
|
|
1901
1943
|
return this.getGraduationStatus(categoryId);
|
|
@@ -1904,7 +1946,7 @@ var MockBackend = class {
|
|
|
1904
1946
|
* Read the D19/§8 backlog-reconciliation status (mock): counts the QUEUED drafts in a
|
|
1905
1947
|
* category that are stale vs current-enough against the current rules-version. The
|
|
1906
1948
|
* mock has no per-draft composed_* stamps on its Review fixtures, so every queued
|
|
1907
|
-
* draft reads as current-enough (composed 0 vs current 0)
|
|
1949
|
+
* draft reads as current-enough (composed 0 vs current 0) - the contract shape is
|
|
1908
1950
|
* exercised; the integer-compare logic is covered by the Go tests.
|
|
1909
1951
|
*/
|
|
1910
1952
|
getScanBacklogStatus(categoryId) {
|
|
@@ -1928,7 +1970,7 @@ var MockBackend = class {
|
|
|
1928
1970
|
};
|
|
1929
1971
|
}
|
|
1930
1972
|
/**
|
|
1931
|
-
* Read the demand-driven pacing state (mock
|
|
1973
|
+
* Read the demand-driven pacing state (mock - M7 Slice B/§8): the cursor + effective
|
|
1932
1974
|
* window/ceiling/interval + each queued draft's classification. The mock has no cursor
|
|
1933
1975
|
* (nothing reviewed) and no composed_* stamps, so every queued draft reads in-window-
|
|
1934
1976
|
* fresh until the window fills, then ahead; the contract shape is exercised (the
|
|
@@ -1968,7 +2010,7 @@ var MockBackend = class {
|
|
|
1968
2010
|
const human = r.author_kind === "human" ? 1 : 0;
|
|
1969
2011
|
return [hard, spec, human, r.rev, r.priority];
|
|
1970
2012
|
}
|
|
1971
|
-
/** Get the ORDERED active rule set (mock)
|
|
2013
|
+
/** Get the ORDERED active rule set (mock) - §7 ladder + category-before-general. */
|
|
1972
2014
|
getRules(params = {}) {
|
|
1973
2015
|
const active = [...this.state.rules.values()].filter((r) => r.status === "active");
|
|
1974
2016
|
const byRank = (a, b) => {
|
|
@@ -1990,9 +2032,17 @@ var MockBackend = class {
|
|
|
1990
2032
|
category = active.filter((r) => r.scope === "category" && r.category_id === params.category_id).sort(byRank);
|
|
1991
2033
|
}
|
|
1992
2034
|
const items = [...category, ...general];
|
|
1993
|
-
return {
|
|
2035
|
+
return {
|
|
2036
|
+
items,
|
|
2037
|
+
total: items.length,
|
|
2038
|
+
house_style_version: 1,
|
|
2039
|
+
category_rules_version: params.category_id ? 1 : 0,
|
|
2040
|
+
rule_high_water: params.category_id ? 1 : 0,
|
|
2041
|
+
composition_token: params.scope ? void 0 : `cmp_fixture_${params.category_id ?? "general"}`,
|
|
2042
|
+
composition_token_expires_at: params.scope ? void 0 : new Date(Date.now() + 6e5).toISOString()
|
|
2043
|
+
};
|
|
1994
2044
|
}
|
|
1995
|
-
/** Save / edit a rule (mock)
|
|
2045
|
+
/** Save / edit a rule (mock) - append-only by supersession (D11). */
|
|
1996
2046
|
saveRule(req) {
|
|
1997
2047
|
const text = req.rule_text.trim();
|
|
1998
2048
|
if (!text) {
|
|
@@ -2081,7 +2131,7 @@ var MockBackend = class {
|
|
|
2081
2131
|
this.recordRuleAudit("supersede", next.id, ruleSnapshotJSON(prior), ruleSnapshotJSON(next));
|
|
2082
2132
|
return next;
|
|
2083
2133
|
}
|
|
2084
|
-
/** Retire a rule (mock)
|
|
2134
|
+
/** Retire a rule (mock) - soft delete, or undefined when unknown. */
|
|
2085
2135
|
retireRule(ruleId) {
|
|
2086
2136
|
const rule = this.state.rules.get(ruleId);
|
|
2087
2137
|
if (!rule) return void 0;
|
|
@@ -2099,7 +2149,7 @@ var MockBackend = class {
|
|
|
2099
2149
|
if (params.entity_id) items = items.filter((e) => e.entity_id === params.entity_id);
|
|
2100
2150
|
return { items, total: items.length };
|
|
2101
2151
|
}
|
|
2102
|
-
/** Undo a rule change (mock)
|
|
2152
|
+
/** Undo a rule change (mock) - restore the prior version; idempotent (re-undo 409). */
|
|
2103
2153
|
undoRuleChange(udoId) {
|
|
2104
2154
|
const entry = this.state.ruleAudit.get(udoId);
|
|
2105
2155
|
if (!entry) throw new NotFoundError({ status: 404, code: "not_found", message: "audit row not found" });
|
|
@@ -2203,7 +2253,7 @@ var MockBackend = class {
|
|
|
2203
2253
|
}
|
|
2204
2254
|
}
|
|
2205
2255
|
/**
|
|
2206
|
-
* Enqueue `front_run_next`
|
|
2256
|
+
* Enqueue `front_run_next` - the signal that the review reached a terminal state
|
|
2207
2257
|
* while the agent was still trying to act on it.
|
|
2208
2258
|
*
|
|
2209
2259
|
* Deduped on (review, terminal state, parent revision) so a retry loop hitting
|
|
@@ -2244,8 +2294,8 @@ var MockBackend = class {
|
|
|
2244
2294
|
}
|
|
2245
2295
|
/**
|
|
2246
2296
|
* Mock-only: mirror an approved draft whose delivery then FAILED at the provider.
|
|
2247
|
-
* This is the case the composing agent was previously never told about
|
|
2248
|
-
* console showed the error and the agent's queue stayed silent
|
|
2297
|
+
* This is the case the composing agent was previously never told about - the
|
|
2298
|
+
* console showed the error and the agent's queue stayed silent - so the loop test
|
|
2249
2299
|
* that matters most drives this path.
|
|
2250
2300
|
*/
|
|
2251
2301
|
simulateSendFailed(reviewId, error = "provider rejected the message") {
|
|
@@ -2330,7 +2380,7 @@ var MockBackend = class {
|
|
|
2330
2380
|
}
|
|
2331
2381
|
/**
|
|
2332
2382
|
* Long-poll for a review event (mock). Offline there is nothing to wait FOR, so it
|
|
2333
|
-
* returns the immediate drain (empty when caught up)
|
|
2383
|
+
* returns the immediate drain (empty when caught up) - the server's "empty on
|
|
2334
2384
|
* timeout" contract.
|
|
2335
2385
|
*/
|
|
2336
2386
|
waitForReviewEvent(params = {}) {
|
|
@@ -2419,7 +2469,7 @@ var MockBackend = class {
|
|
|
2419
2469
|
html: opts.html ?? null,
|
|
2420
2470
|
extracted_text: opts.text.trim() || null,
|
|
2421
2471
|
extracted_html: opts.html?.trim() || null,
|
|
2422
|
-
message_id: `<${id}@${address.split("@")[1] ??
|
|
2472
|
+
message_id: `<${id}@${address.split("@")[1] ?? PAID_SHARED_DOMAIN}>`,
|
|
2423
2473
|
folder: opts.direction === "inbound" ? "INBOX" : "Sent",
|
|
2424
2474
|
seen: opts.direction === "outbound",
|
|
2425
2475
|
date: now()
|
|
@@ -2535,10 +2585,10 @@ var MockBackend = class {
|
|
|
2535
2585
|
if (offset + page.length < total) result.next_cursor = String(offset + page.length);
|
|
2536
2586
|
return result;
|
|
2537
2587
|
}
|
|
2538
|
-
listThreads(address) {
|
|
2588
|
+
listThreads(address, params = {}) {
|
|
2539
2589
|
address = this.addrOf(address);
|
|
2540
2590
|
const items = this.threadsFor(address);
|
|
2541
|
-
return
|
|
2591
|
+
return this.paginateThreads(items, params);
|
|
2542
2592
|
}
|
|
2543
2593
|
/** Thread-level search (subject / snippet / participant substring). */
|
|
2544
2594
|
searchThreads(address, params) {
|
|
@@ -2547,7 +2597,17 @@ var MockBackend = class {
|
|
|
2547
2597
|
const items = this.threadsFor(address).filter(
|
|
2548
2598
|
(t) => t.subject.toLowerCase().includes(q) || t.snippet.toLowerCase().includes(q) || t.participants.join(" ").toLowerCase().includes(q)
|
|
2549
2599
|
);
|
|
2550
|
-
return
|
|
2600
|
+
return this.paginateThreads(items, params);
|
|
2601
|
+
}
|
|
2602
|
+
paginateThreads(items, params) {
|
|
2603
|
+
const total = items.length;
|
|
2604
|
+
const cursorOffset = params.cursor === void 0 ? void 0 : Number(params.cursor);
|
|
2605
|
+
const offset = params.offset ?? (Number.isFinite(cursorOffset) ? cursorOffset : 0);
|
|
2606
|
+
const limit = params.limit ?? 25;
|
|
2607
|
+
const page = items.slice(offset, offset + limit);
|
|
2608
|
+
const result = { items: page, total };
|
|
2609
|
+
if (offset + page.length < total) result.next_cursor = String(offset + page.length);
|
|
2610
|
+
return result;
|
|
2551
2611
|
}
|
|
2552
2612
|
/** Fetch one thread (with messages, oldest-first) by id under an inbox. */
|
|
2553
2613
|
getThread(address, threadId) {
|
|
@@ -2619,17 +2679,18 @@ var MockBackend = class {
|
|
|
2619
2679
|
arr.push(m);
|
|
2620
2680
|
byThread.set(m.thread_id, arr);
|
|
2621
2681
|
}
|
|
2622
|
-
const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms));
|
|
2682
|
+
const items = [...byThread.entries()].map(([id, ms]) => this.buildThread(address, id, ms, true));
|
|
2623
2683
|
items.sort((a, b) => b.last_message_at.localeCompare(a.last_message_at));
|
|
2624
2684
|
return items;
|
|
2625
2685
|
}
|
|
2626
|
-
buildThread(address, id, ms) {
|
|
2686
|
+
buildThread(address, id, ms, summary = false) {
|
|
2627
2687
|
const sorted = [...ms].sort((a, b) => a.date.localeCompare(b.date));
|
|
2628
2688
|
const last = sorted[sorted.length - 1];
|
|
2629
2689
|
const seen = /* @__PURE__ */ new Set();
|
|
2630
2690
|
const participants = [];
|
|
2631
|
-
|
|
2632
|
-
|
|
2691
|
+
const participantMessages = summary ? [last] : sorted;
|
|
2692
|
+
for (const m of participantMessages) {
|
|
2693
|
+
for (const a of [m.from, ...m.to, ...m.cc ?? [], ...m.reply_to ?? []]) {
|
|
2633
2694
|
const key = a.email.toLowerCase();
|
|
2634
2695
|
if (seen.has(key)) continue;
|
|
2635
2696
|
seen.add(key);
|
|
@@ -2643,7 +2704,10 @@ var MockBackend = class {
|
|
|
2643
2704
|
participants,
|
|
2644
2705
|
message_count: sorted.length,
|
|
2645
2706
|
last_message_at: last.date,
|
|
2646
|
-
snippet: (last.text ?? last.html ?? "").slice(0, 120)
|
|
2707
|
+
snippet: (last.text ?? last.html ?? "").slice(0, 120),
|
|
2708
|
+
unread: !last.seen,
|
|
2709
|
+
last_message_has_attachments: (this.state.attachments.get(last.id)?.length ?? 0) > 0,
|
|
2710
|
+
last_message_id: last.id
|
|
2647
2711
|
};
|
|
2648
2712
|
}
|
|
2649
2713
|
/**
|
|
@@ -2803,24 +2867,34 @@ ${text}`)) {
|
|
|
2803
2867
|
return this.state.contactLists.delete(entryId);
|
|
2804
2868
|
}
|
|
2805
2869
|
// ---- domains (Slice 5) ------------------------------------------------
|
|
2806
|
-
/**
|
|
2870
|
+
/** Add a delegated domain and return only the customer-published nameservers. */
|
|
2807
2871
|
onboardDomain(req) {
|
|
2808
2872
|
assertProjectMatch(req.project_id);
|
|
2809
2873
|
const name = req.domain.trim().toLowerCase();
|
|
2810
2874
|
const existing = this.state.domains.get(name);
|
|
2811
2875
|
if (existing) return { ...existing };
|
|
2812
|
-
const mode =
|
|
2876
|
+
const mode = "ns_delegated";
|
|
2813
2877
|
const domain = {
|
|
2814
2878
|
id: rid("dom"),
|
|
2815
2879
|
domain: name,
|
|
2816
2880
|
mode,
|
|
2817
|
-
verification_status:
|
|
2818
|
-
dkim_status:
|
|
2819
|
-
shared:
|
|
2881
|
+
verification_status: "verifying",
|
|
2882
|
+
dkim_status: "configured",
|
|
2883
|
+
shared: false,
|
|
2820
2884
|
created_at: now(),
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2885
|
+
delegation_ns: domainDelegationNS(name),
|
|
2886
|
+
instruction: "Add the nameserver entries at your domain provider. We check automatically; use Recheck DNS for an immediate check.",
|
|
2887
|
+
readiness: {
|
|
2888
|
+
status: "waiting_for_dns",
|
|
2889
|
+
label: "Waiting for DNS",
|
|
2890
|
+
summary: "We have not confirmed your nameserver entries yet. Add them at your domain provider; we will finish setup automatically.",
|
|
2891
|
+
reason: "dns_entries_unconfirmed",
|
|
2892
|
+
action_required_by: "customer",
|
|
2893
|
+
next_action: "check_dns_entries",
|
|
2894
|
+
ready_for_inboxes: false,
|
|
2895
|
+
poll_after_seconds: 30,
|
|
2896
|
+
inboxes: { scope: "agent", total: 0, ready: 0, setting_up: 0, needs_attention: 0 }
|
|
2897
|
+
}
|
|
2824
2898
|
};
|
|
2825
2899
|
this.state.domains.set(name, domain);
|
|
2826
2900
|
return { ...domain };
|
|
@@ -2868,6 +2942,129 @@ ${text}`)) {
|
|
|
2868
2942
|
const job = this.state.jobs.get(jobId);
|
|
2869
2943
|
return job ? { ...job } : void 0;
|
|
2870
2944
|
}
|
|
2945
|
+
// ---- commerce (quote/request/status; no human approval mutation) ------
|
|
2946
|
+
quoteDomain(req) {
|
|
2947
|
+
const domain = req.domain.trim().toLowerCase();
|
|
2948
|
+
return {
|
|
2949
|
+
object: "domain_quote",
|
|
2950
|
+
domain,
|
|
2951
|
+
available: !domain.startsWith("unavailable."),
|
|
2952
|
+
currency: "usd",
|
|
2953
|
+
quote_cents: 2500,
|
|
2954
|
+
renewal_cents: 2500,
|
|
2955
|
+
premium: false,
|
|
2956
|
+
quote_expires_at: new Date(Date.now() + 15 * 60 * 1e3).toISOString(),
|
|
2957
|
+
blockers: []
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
requestDomainPurchase(req) {
|
|
2961
|
+
const idem = `domain_purchase:${req.idempotency_key.trim()}`;
|
|
2962
|
+
const replayId = this.state.commerceByIdempotency.get(idem);
|
|
2963
|
+
if (replayId) return { ...this.state.commerceRequests.get(replayId) };
|
|
2964
|
+
const quote = this.quoteDomain({ domain: req.domain });
|
|
2965
|
+
const timestamp = now();
|
|
2966
|
+
const id = rid("creq");
|
|
2967
|
+
const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
|
|
2968
|
+
const request = {
|
|
2969
|
+
object: "commerce_request",
|
|
2970
|
+
id,
|
|
2971
|
+
kind: "domain_purchase",
|
|
2972
|
+
state: "awaiting_human_approval",
|
|
2973
|
+
domain: quote.domain,
|
|
2974
|
+
domain_scope: req.scope ?? "org",
|
|
2975
|
+
rationale: req.rationale,
|
|
2976
|
+
currency: quote.currency,
|
|
2977
|
+
quote_cents: quote.quote_cents,
|
|
2978
|
+
renewal_cents: quote.renewal_cents,
|
|
2979
|
+
quote_expires_at: quote.quote_expires_at,
|
|
2980
|
+
auto_renew: req.auto_renew ?? true,
|
|
2981
|
+
blocker_code: "human_approval_required",
|
|
2982
|
+
blockers: [
|
|
2983
|
+
{
|
|
2984
|
+
code: "human_approval_required",
|
|
2985
|
+
message: "A human billing administrator must approve this domain purchase.",
|
|
2986
|
+
manage_url: approvalUrl
|
|
2987
|
+
}
|
|
2988
|
+
],
|
|
2989
|
+
approval_url: approvalUrl,
|
|
2990
|
+
agent_next_action: "Share approval_url with the human, then poll this request after approval.",
|
|
2991
|
+
retry_safe: true,
|
|
2992
|
+
poll_after_seconds: 10,
|
|
2993
|
+
version: 1,
|
|
2994
|
+
created_at: timestamp,
|
|
2995
|
+
updated_at: timestamp
|
|
2996
|
+
};
|
|
2997
|
+
this.state.commerceRequests.set(id, request);
|
|
2998
|
+
this.state.commerceByIdempotency.set(idem, id);
|
|
2999
|
+
return { ...request };
|
|
3000
|
+
}
|
|
3001
|
+
requestPlanChange(req) {
|
|
3002
|
+
const idem = `plan_change:${req.idempotency_key.trim()}`;
|
|
3003
|
+
const replayId = this.state.commerceByIdempotency.get(idem);
|
|
3004
|
+
if (replayId) return { ...this.state.commerceRequests.get(replayId) };
|
|
3005
|
+
const timestamp = now();
|
|
3006
|
+
const id = rid("creq");
|
|
3007
|
+
const approvalUrl = `https://app.extrovert.dev/commerce/requests/${id}`;
|
|
3008
|
+
const request = {
|
|
3009
|
+
object: "commerce_request",
|
|
3010
|
+
id,
|
|
3011
|
+
kind: "plan_change",
|
|
3012
|
+
state: "awaiting_human_approval",
|
|
3013
|
+
target_plan: req.target_plan,
|
|
3014
|
+
current_plan: "developer",
|
|
3015
|
+
rationale: req.rationale,
|
|
3016
|
+
currency: "usd",
|
|
3017
|
+
quote_cents: 0,
|
|
3018
|
+
renewal_cents: 0,
|
|
3019
|
+
auto_renew: true,
|
|
3020
|
+
blocker_code: "human_approval_required",
|
|
3021
|
+
blockers: [
|
|
3022
|
+
{
|
|
3023
|
+
code: "human_approval_required",
|
|
3024
|
+
message: "A human billing administrator must approve this plan change.",
|
|
3025
|
+
manage_url: approvalUrl
|
|
3026
|
+
}
|
|
3027
|
+
],
|
|
3028
|
+
approval_url: approvalUrl,
|
|
3029
|
+
agent_next_action: "Share approval_url with the human, then poll this request after approval.",
|
|
3030
|
+
retry_safe: true,
|
|
3031
|
+
poll_after_seconds: 10,
|
|
3032
|
+
version: 1,
|
|
3033
|
+
created_at: timestamp,
|
|
3034
|
+
updated_at: timestamp
|
|
3035
|
+
};
|
|
3036
|
+
this.state.commerceRequests.set(id, request);
|
|
3037
|
+
this.state.commerceByIdempotency.set(idem, id);
|
|
3038
|
+
return { ...request };
|
|
3039
|
+
}
|
|
3040
|
+
getCommerceRequest(requestId) {
|
|
3041
|
+
const request = this.state.commerceRequests.get(requestId);
|
|
3042
|
+
return request ? { ...request } : void 0;
|
|
3043
|
+
}
|
|
3044
|
+
cancelCommerceRequest(requestId) {
|
|
3045
|
+
const request = this.state.commerceRequests.get(requestId);
|
|
3046
|
+
if (!request) return void 0;
|
|
3047
|
+
if (!["awaiting_human_approval", "blocked", "approved", "payment_action_required", "payment_failed"].includes(request.state)) {
|
|
3048
|
+
throw new ValidationError({ status: 409, code: "conflict", message: "this request can no longer be cancelled" });
|
|
3049
|
+
}
|
|
3050
|
+
request.state = "cancelled";
|
|
3051
|
+
request.blocker_code = void 0;
|
|
3052
|
+
request.blockers = [];
|
|
3053
|
+
request.agent_next_action = "The request is cancelled. Create a new request only if the purchase is still needed.";
|
|
3054
|
+
request.version += 1;
|
|
3055
|
+
request.updated_at = now();
|
|
3056
|
+
return { ...request };
|
|
3057
|
+
}
|
|
3058
|
+
listCommerceRequests(params = {}) {
|
|
3059
|
+
let items = [...this.state.commerceRequests.values()];
|
|
3060
|
+
items.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
3061
|
+
const total = items.length;
|
|
3062
|
+
const offset = params.page ? Math.max(0, Number.parseInt(params.page, 10) || 0) : 0;
|
|
3063
|
+
const limit = Math.max(1, Math.min(params.limit ?? 50, 100));
|
|
3064
|
+
const page = items.slice(offset, offset + limit).map((request) => ({ ...request }));
|
|
3065
|
+
const next = offset + page.length;
|
|
3066
|
+
return { items: page, total, next_cursor: next < total ? String(next) : void 0 };
|
|
3067
|
+
}
|
|
2871
3068
|
// ---- suppressions (recipient opt-outs / list-unsubscribe) --------------
|
|
2872
3069
|
/**
|
|
2873
3070
|
* Pre-check whether the caller's org suppresses a recipient (mirrors
|
|
@@ -2911,7 +3108,7 @@ ${text}`)) {
|
|
|
2911
3108
|
/**
|
|
2912
3109
|
* Reject the WHOLE send if ANY recipient has an active org-scope suppression,
|
|
2913
3110
|
* naming exactly the suppressed addresses (never the scope/origin) so the caller
|
|
2914
|
-
* can drop them and retry
|
|
3111
|
+
* can drop them and retry - mirroring the live `recipient_suppressed` (422) path.
|
|
2915
3112
|
*/
|
|
2916
3113
|
enforceSuppression(recipients) {
|
|
2917
3114
|
const active = new Set(
|
|
@@ -2994,16 +3191,6 @@ function redactSecret(w) {
|
|
|
2994
3191
|
const { secret: _omit, ...rest } = w;
|
|
2995
3192
|
return rest;
|
|
2996
3193
|
}
|
|
2997
|
-
function domainRecordSet(domain) {
|
|
2998
|
-
const dkimSuffix = domain.replace(/\./g, "-");
|
|
2999
|
-
return [
|
|
3000
|
-
{ name: domain, type: "MX", value: "smtp.extrovert.dev", priority: 10, ttl: 3600 },
|
|
3001
|
-
{ name: domain, type: "TXT", value: "v=spf1 include:spf.protection.outlook.com -all", ttl: 3600 },
|
|
3002
|
-
{ name: `_dmarc.${domain}`, type: "TXT", value: "v=DMARC1; p=none; rua=mailto:dmarc@smtp.extrovert.dev", ttl: 3600 },
|
|
3003
|
-
{ name: `selector1._domainkey.${domain}`, type: "CNAME", value: `selector1-${dkimSuffix}._domainkey.azurecomm.net`, ttl: 3600 },
|
|
3004
|
-
{ name: `selector2._domainkey.${domain}`, type: "CNAME", value: `selector2-${dkimSuffix}._domainkey.azurecomm.net`, ttl: 3600 }
|
|
3005
|
-
];
|
|
3006
|
-
}
|
|
3007
3194
|
function domainDelegationNS(domain) {
|
|
3008
3195
|
return [
|
|
3009
3196
|
{ name: domain, type: "NS", value: "ns1.extrovert.dev", ttl: 300 },
|
|
@@ -3387,8 +3574,11 @@ var HttpTransport = class {
|
|
|
3387
3574
|
signal
|
|
3388
3575
|
});
|
|
3389
3576
|
}
|
|
3390
|
-
listDomains(signal) {
|
|
3391
|
-
return this.call({ method: "GET", path: "/v1/domains", signal });
|
|
3577
|
+
listDomains(signal, params = {}) {
|
|
3578
|
+
return this.call({ method: "GET", path: "/v1/domains", query: params, signal });
|
|
3579
|
+
}
|
|
3580
|
+
listDomainEvents(domain, params, signal) {
|
|
3581
|
+
return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}/events`, query: params, signal });
|
|
3392
3582
|
}
|
|
3393
3583
|
getDomain(domain, signal) {
|
|
3394
3584
|
return this.call({ method: "GET", path: `/v1/domains/${encodeURIComponent(domain)}`, signal });
|
|
@@ -3396,6 +3586,8 @@ var HttpTransport = class {
|
|
|
3396
3586
|
onboardDomain(req, signal) {
|
|
3397
3587
|
return this.call({ method: "POST", path: "/v1/domains", body: req, signal });
|
|
3398
3588
|
}
|
|
3589
|
+
// Delegated domains perform an immediate authoritative DNS check. Inspect
|
|
3590
|
+
// delegation.status separately from mail readiness; 429 requests may be retried.
|
|
3399
3591
|
verifyDomain(domain, signal) {
|
|
3400
3592
|
return this.call({
|
|
3401
3593
|
method: "POST",
|
|
@@ -3420,6 +3612,54 @@ var HttpTransport = class {
|
|
|
3420
3612
|
getJob(jobId, signal) {
|
|
3421
3613
|
return this.call({ method: "GET", path: `/v1/jobs/${encodeURIComponent(jobId)}`, signal });
|
|
3422
3614
|
}
|
|
3615
|
+
quoteDomain(req, signal) {
|
|
3616
|
+
return this.call({ method: "POST", path: "/v1/commerce/domain-quotes", body: req, signal });
|
|
3617
|
+
}
|
|
3618
|
+
requestDomainPurchase(req, signal) {
|
|
3619
|
+
const { idempotency_key, ...body } = req;
|
|
3620
|
+
return this.call({
|
|
3621
|
+
method: "POST",
|
|
3622
|
+
path: "/v1/commerce/requests/domain-purchases",
|
|
3623
|
+
body,
|
|
3624
|
+
idempotencyKey: idempotency_key,
|
|
3625
|
+
signal
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
requestPlanChange(req, signal) {
|
|
3629
|
+
const { idempotency_key, ...body } = req;
|
|
3630
|
+
return this.call({
|
|
3631
|
+
method: "POST",
|
|
3632
|
+
path: "/v1/commerce/requests/plan-changes",
|
|
3633
|
+
body,
|
|
3634
|
+
idempotencyKey: idempotency_key,
|
|
3635
|
+
signal
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
getCommerceRequest(requestId, signal) {
|
|
3639
|
+
return this.call({
|
|
3640
|
+
method: "GET",
|
|
3641
|
+
path: `/v1/commerce/requests/${encodeURIComponent(requestId)}`,
|
|
3642
|
+
signal
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
cancelCommerceRequest(requestId, signal) {
|
|
3646
|
+
return this.call({
|
|
3647
|
+
method: "POST",
|
|
3648
|
+
path: `/v1/commerce/requests/${encodeURIComponent(requestId)}/cancel`,
|
|
3649
|
+
signal
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
listCommerceRequests(params, signal) {
|
|
3653
|
+
return this.call({
|
|
3654
|
+
method: "GET",
|
|
3655
|
+
path: "/v1/commerce/requests",
|
|
3656
|
+
query: {
|
|
3657
|
+
limit: params.limit,
|
|
3658
|
+
page: params.page
|
|
3659
|
+
},
|
|
3660
|
+
signal
|
|
3661
|
+
});
|
|
3662
|
+
}
|
|
3423
3663
|
submitForReview(address, req, signal) {
|
|
3424
3664
|
return this.call({
|
|
3425
3665
|
method: "POST",
|
|
@@ -3598,7 +3838,13 @@ var HttpTransport = class {
|
|
|
3598
3838
|
});
|
|
3599
3839
|
}
|
|
3600
3840
|
saveRule(req, signal) {
|
|
3601
|
-
return this.call({
|
|
3841
|
+
return this.call({
|
|
3842
|
+
method: "PUT",
|
|
3843
|
+
path: "/v1/rules",
|
|
3844
|
+
body: withoutIdempotencyKey(req),
|
|
3845
|
+
idempotencyKey: req.idempotency_key,
|
|
3846
|
+
signal
|
|
3847
|
+
});
|
|
3602
3848
|
}
|
|
3603
3849
|
promoteRule(ruleId, toScope, signal) {
|
|
3604
3850
|
return this.call({
|
|
@@ -3772,8 +4018,8 @@ var MockTransport = class {
|
|
|
3772
4018
|
async searchMessages(address, params) {
|
|
3773
4019
|
return this.backend.searchMessages(address, params);
|
|
3774
4020
|
}
|
|
3775
|
-
async listThreads(address,
|
|
3776
|
-
return this.backend.listThreads(address);
|
|
4021
|
+
async listThreads(address, params) {
|
|
4022
|
+
return this.backend.listThreads(address, params);
|
|
3777
4023
|
}
|
|
3778
4024
|
async searchThreads(address, params) {
|
|
3779
4025
|
return this.backend.searchThreads(address, params);
|
|
@@ -3837,8 +4083,16 @@ var MockTransport = class {
|
|
|
3837
4083
|
if (!row) throw notFound("suppression", id);
|
|
3838
4084
|
return row;
|
|
3839
4085
|
}
|
|
3840
|
-
async listDomains() {
|
|
3841
|
-
|
|
4086
|
+
async listDomains(_signal, params = {}) {
|
|
4087
|
+
const page = this.backend.listDomains();
|
|
4088
|
+
const offset = Number(params.page ?? 0);
|
|
4089
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid page cursor");
|
|
4090
|
+
const items = page.items.slice(offset, offset + (params.limit ?? 50));
|
|
4091
|
+
return { items, total: page.total, next_cursor: offset + items.length < page.items.length ? String(offset + items.length) : void 0 };
|
|
4092
|
+
}
|
|
4093
|
+
async listDomainEvents(domain, params) {
|
|
4094
|
+
if (!this.backend.getDomain(domain)) throw notFound("domain", domain);
|
|
4095
|
+
return { items: [], next_cursor: params.after ?? "0", has_more: false, poll_after_seconds: 30 };
|
|
3842
4096
|
}
|
|
3843
4097
|
async getDomain(domain) {
|
|
3844
4098
|
const d = this.backend.getDomain(domain);
|
|
@@ -3863,6 +4117,28 @@ var MockTransport = class {
|
|
|
3863
4117
|
if (!job) throw notFound("job", jobId);
|
|
3864
4118
|
return job;
|
|
3865
4119
|
}
|
|
4120
|
+
async quoteDomain(req) {
|
|
4121
|
+
return this.backend.quoteDomain(req);
|
|
4122
|
+
}
|
|
4123
|
+
async requestDomainPurchase(req) {
|
|
4124
|
+
return this.backend.requestDomainPurchase(req);
|
|
4125
|
+
}
|
|
4126
|
+
async requestPlanChange(req) {
|
|
4127
|
+
return this.backend.requestPlanChange(req);
|
|
4128
|
+
}
|
|
4129
|
+
async getCommerceRequest(requestId) {
|
|
4130
|
+
const request = this.backend.getCommerceRequest(requestId);
|
|
4131
|
+
if (!request) throw notFound("commerce request", requestId);
|
|
4132
|
+
return request;
|
|
4133
|
+
}
|
|
4134
|
+
async cancelCommerceRequest(requestId) {
|
|
4135
|
+
const request = this.backend.cancelCommerceRequest(requestId);
|
|
4136
|
+
if (!request) throw notFound("commerce request", requestId);
|
|
4137
|
+
return request;
|
|
4138
|
+
}
|
|
4139
|
+
async listCommerceRequests(params) {
|
|
4140
|
+
return this.backend.listCommerceRequests(params);
|
|
4141
|
+
}
|
|
3866
4142
|
async submitForReview(address, req) {
|
|
3867
4143
|
return this.backend.submitForReview(address, req);
|
|
3868
4144
|
}
|
|
@@ -4017,6 +4293,49 @@ function filenameFromDisposition(disposition) {
|
|
|
4017
4293
|
return m ? decodeURIComponent(m[1].trim()) : "";
|
|
4018
4294
|
}
|
|
4019
4295
|
|
|
4296
|
+
// src/domain-wait.ts
|
|
4297
|
+
async function waitForDomain(get, options = {}) {
|
|
4298
|
+
const seconds = options.timeout_seconds ?? 45;
|
|
4299
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 50) throw new Error("timeout_seconds must be an integer between 0 and 50");
|
|
4300
|
+
const deadline = AbortSignal.timeout(Math.max(1, seconds * 1e3));
|
|
4301
|
+
const signal = options.signal ? AbortSignal.any([options.signal, deadline]) : deadline;
|
|
4302
|
+
let latest;
|
|
4303
|
+
const result = (outcome) => ({
|
|
4304
|
+
domain: latest,
|
|
4305
|
+
outcome,
|
|
4306
|
+
resume_after_seconds: outcome === "timed_out" ? Math.max(5, Math.min(60, latest?.readiness?.poll_after_seconds ?? 30)) : 0
|
|
4307
|
+
});
|
|
4308
|
+
try {
|
|
4309
|
+
const singleCheckDeadline = seconds === 0 ? AbortSignal.timeout(1e4) : void 0;
|
|
4310
|
+
latest = await get(singleCheckDeadline ? options.signal ? AbortSignal.any([options.signal, singleCheckDeadline]) : singleCheckDeadline : signal);
|
|
4311
|
+
for (; ; ) {
|
|
4312
|
+
const r = latest.readiness;
|
|
4313
|
+
if (!r) return result("status_unavailable");
|
|
4314
|
+
if (r.ready_for_inboxes) return result("ready");
|
|
4315
|
+
if (r.action_required_by === "customer") return result("action_required");
|
|
4316
|
+
if (r.status === "needs_attention") return result("needs_attention");
|
|
4317
|
+
if (seconds === 0) return result("timed_out");
|
|
4318
|
+
await new Promise((resolve, reject) => {
|
|
4319
|
+
signal.throwIfAborted();
|
|
4320
|
+
const abort = () => {
|
|
4321
|
+
clearTimeout(timer);
|
|
4322
|
+
reject(signal.reason);
|
|
4323
|
+
};
|
|
4324
|
+
const timer = setTimeout(() => {
|
|
4325
|
+
signal.removeEventListener("abort", abort);
|
|
4326
|
+
resolve();
|
|
4327
|
+
}, Math.max(5, Math.min(60, r.poll_after_seconds || 30)) * 1e3);
|
|
4328
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
4329
|
+
});
|
|
4330
|
+
latest = await get(signal);
|
|
4331
|
+
}
|
|
4332
|
+
} catch (error) {
|
|
4333
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
4334
|
+
if (deadline.aborted && latest) return result("timed_out");
|
|
4335
|
+
throw error;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4020
4339
|
// src/key-tier.ts
|
|
4021
4340
|
var AGENT_HEAD = "pk_agent_";
|
|
4022
4341
|
function parseKeyTier(apiKey) {
|
|
@@ -4101,8 +4420,8 @@ var InboxHandle = class {
|
|
|
4101
4420
|
* Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
|
|
4102
4421
|
* human has to approve it and NOTHING has been delivered yet; anything else was
|
|
4103
4422
|
* delivered. Under the default `require_review` policy a call WITHOUT an
|
|
4104
|
-
* `intent` raises `IntentRequiredError` (422) instead
|
|
4105
|
-
* queued
|
|
4423
|
+
* `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
|
|
4424
|
+
* queued: so pass one, or read `inbox.record.effective_review_policy` first.
|
|
4106
4425
|
*/
|
|
4107
4426
|
send(req, signal) {
|
|
4108
4427
|
return this.transport.send(this.ref, req, signal);
|
|
@@ -4112,7 +4431,7 @@ var InboxHandle = class {
|
|
|
4112
4431
|
* the latest message) or `message_id` (reply to that message); the server
|
|
4113
4432
|
* derives To / Subject / In-Reply-To / References. Set `reply_all` to reply to
|
|
4114
4433
|
* every thread recipient. Returns the same three-way {@link SendOutcome} as
|
|
4115
|
-
* {@link send}
|
|
4434
|
+
* {@link send}: a reply is governed by the review policy too.
|
|
4116
4435
|
*/
|
|
4117
4436
|
reply(req, signal) {
|
|
4118
4437
|
return this.transport.reply(this.ref, req, signal);
|
|
@@ -4121,7 +4440,7 @@ var InboxHandle = class {
|
|
|
4121
4440
|
* Forward a message in this inbox to new recipients, preserving the original.
|
|
4122
4441
|
*
|
|
4123
4442
|
* A forward is an outbound message to arbitrary NEW recipients that quotes an
|
|
4124
|
-
* inbound thread, so it is governed by the review policy exactly like a send
|
|
4443
|
+
* inbound thread, so it is governed by the review policy exactly like a send :
|
|
4125
4444
|
* same {@link SendOutcome} union, same `intent` requirement.
|
|
4126
4445
|
*/
|
|
4127
4446
|
forward(messageId, req, signal) {
|
|
@@ -4285,7 +4604,7 @@ var ListPage = class _ListPage {
|
|
|
4285
4604
|
this.nextCursor = raw.next_cursor;
|
|
4286
4605
|
}
|
|
4287
4606
|
/**
|
|
4288
|
-
* Fetch the next page. Throws if there is none
|
|
4607
|
+
* Fetch the next page. Throws if there is none - guard with {@link hasMore}.
|
|
4289
4608
|
*/
|
|
4290
4609
|
async nextPage(signal) {
|
|
4291
4610
|
if (!this.hasMore || this.nextCursor === null) {
|
|
@@ -4373,8 +4692,8 @@ var ProjectInboxes = class {
|
|
|
4373
4692
|
* Returns a three-way {@link SendOutcome}: `kind:"queued_for_review"` means a
|
|
4374
4693
|
* human has to approve it and NOTHING has been delivered yet; anything else was
|
|
4375
4694
|
* delivered. Under the default `require_review` policy a call WITHOUT an
|
|
4376
|
-
* `intent` raises `IntentRequiredError` (422) instead
|
|
4377
|
-
* queued
|
|
4695
|
+
* `intent` raises `IntentRequiredError` (422) instead: nothing sent, nothing
|
|
4696
|
+
* queued: so pass one, or read `inbox.record.effective_review_policy` first.
|
|
4378
4697
|
*/
|
|
4379
4698
|
send(projectId, inboxId, req, signal) {
|
|
4380
4699
|
return this.ctx.transport.send(this.ref(projectId, inboxId), req, signal);
|
|
@@ -4440,14 +4759,14 @@ var ProjectInboxes = class {
|
|
|
4440
4759
|
*
|
|
4441
4760
|
* The frozen contract project-prefixes ONLY the inbox collection/item/credentials
|
|
4442
4761
|
* routes (`/v1/projects/{project_id}/inboxes[/{inbox_id}][/credentials]`); the
|
|
4443
|
-
* send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path
|
|
4762
|
+
* send/reply/forward/message/thread/wait sub-ops have NO project-prefixed path :
|
|
4444
4763
|
* they address the inbox by its opaque id directly (`/v1/inboxes/{inbox_id}/…`),
|
|
4445
4764
|
* where the project is implicit in (and enforced by) the inbox id server-side.
|
|
4446
4765
|
*
|
|
4447
4766
|
* So for these sub-ops `projectId` cannot be carried on the URL and is NOT a URL
|
|
4448
4767
|
* selector. The adversarial review flagged that silently discarding it makes the
|
|
4449
4768
|
* signature misleading. CHOICE: keep the arg (dropping it would break the chain's
|
|
4450
|
-
* symmetry with create/list/get/update/delete
|
|
4769
|
+
* symmetry with create/list/get/update/delete: the more disruptive option) but
|
|
4451
4770
|
* VALIDATE it rather than ignore it. We reject the two client mistakes we can catch
|
|
4452
4771
|
* without a round-trip:
|
|
4453
4772
|
* - a blank / whitespace-only `projectId` (a required selector everywhere else in
|
|
@@ -4486,8 +4805,9 @@ var Inboxes = class {
|
|
|
4486
4805
|
this.ctx = ctx;
|
|
4487
4806
|
}
|
|
4488
4807
|
/**
|
|
4489
|
-
* Create an inbox. The default path
|
|
4490
|
-
* `
|
|
4808
|
+
* Create an inbox. The default path creates an address on `extrovertmail.com`
|
|
4809
|
+
* for paid accounts or `free.extrovertmail.com` for free signups, so it returns
|
|
4810
|
+
* a live inbox in one call.
|
|
4491
4811
|
*
|
|
4492
4812
|
* Pass `metadata` to attach arbitrary key-value data, and `client_id` for idempotent creation
|
|
4493
4813
|
* (re-calling with the same id returns the same inbox, with its metadata replayed verbatim).
|
|
@@ -4497,12 +4817,12 @@ var Inboxes = class {
|
|
|
4497
4817
|
return new InboxHandle(this.ctx.transport, inbox.address, this.ctx.handleOptions, inbox);
|
|
4498
4818
|
}
|
|
4499
4819
|
/**
|
|
4500
|
-
* List inboxes visible to the calling key (the bare curl-sugar surface
|
|
4820
|
+
* List inboxes visible to the calling key (the bare curl-sugar surface: resolves
|
|
4501
4821
|
* to the key's default project). An org-tier key has no single default project, so
|
|
4502
4822
|
* the bare list is ambiguous: fail fast client-side with a BreadthRequiredError that
|
|
4503
4823
|
* names the next call, matching the MCP surface, instead of round-tripping to a 400.
|
|
4504
4824
|
* Use `extrovert.projects.inboxes.list("<project_id>")` or `"-"` (org subtree) for
|
|
4505
|
-
* an org key. The check is advisory
|
|
4825
|
+
* an org key. The check is advisory: the server stays authoritative.
|
|
4506
4826
|
*/
|
|
4507
4827
|
list(params = {}, signal) {
|
|
4508
4828
|
if (tierNeedsExplicitBreadth(this.ctx.keyTier)) {
|
|
@@ -4598,10 +4918,22 @@ var Threads = class {
|
|
|
4598
4918
|
constructor(ctx) {
|
|
4599
4919
|
this.ctx = ctx;
|
|
4600
4920
|
}
|
|
4921
|
+
/** List conversations newest-active first. Pass `next_cursor` back as `cursor` for the next page. */
|
|
4922
|
+
list(inbox, params = {}, signal) {
|
|
4923
|
+
return this.ctx.transport.listThreads(inbox, params, signal);
|
|
4924
|
+
}
|
|
4925
|
+
/** Search thread subjects, snippets, and participants. Cursor pagination matches {@link list}. */
|
|
4926
|
+
search(inbox, params, signal) {
|
|
4927
|
+
return this.ctx.transport.searchThreads(inbox, params, signal);
|
|
4928
|
+
}
|
|
4601
4929
|
/** Fetch one thread (+ its messages, oldest-first) by id under its owning inbox address. */
|
|
4602
4930
|
get(inbox, threadId, signal) {
|
|
4603
4931
|
return this.ctx.transport.getThread(inbox, threadId, signal);
|
|
4604
4932
|
}
|
|
4933
|
+
/** Reply in a thread; recipients and RFC reply headers are derived server-side. */
|
|
4934
|
+
reply(inbox, req, signal) {
|
|
4935
|
+
return this.ctx.transport.reply(inbox, req, signal);
|
|
4936
|
+
}
|
|
4605
4937
|
/**
|
|
4606
4938
|
* Delete an entire thread (every message): move to Trash (default) or
|
|
4607
4939
|
* permanently remove when `expunge` is true. `inbox` is the owning address.
|
|
@@ -4663,7 +4995,7 @@ var Suppressions = class {
|
|
|
4663
4995
|
}
|
|
4664
4996
|
/**
|
|
4665
4997
|
* Pre-check whether the caller's org already suppresses a recipient, BEFORE
|
|
4666
|
-
* composing. `suppressed: true` means a send to them would be rejected
|
|
4998
|
+
* composing. `suppressed: true` means a send to them would be rejected: skip
|
|
4667
4999
|
* that recipient. Returns the matching org rows too (never a global/shared row).
|
|
4668
5000
|
*/
|
|
4669
5001
|
precheck(recipient, signal) {
|
|
@@ -4687,17 +5019,25 @@ var Domains = class {
|
|
|
4687
5019
|
this.ctx = ctx;
|
|
4688
5020
|
}
|
|
4689
5021
|
/** List the customer's onboarded domains and their status. */
|
|
4690
|
-
list(signal) {
|
|
4691
|
-
return this.ctx.transport.listDomains(
|
|
5022
|
+
list(paramsOrSignal = {}, signal) {
|
|
5023
|
+
if ("aborted" in paramsOrSignal) return this.ctx.transport.listDomains(paramsOrSignal);
|
|
5024
|
+
return this.ctx.transport.listDomains(signal, paramsOrSignal);
|
|
4692
5025
|
}
|
|
4693
|
-
/** Get one domain's detail
|
|
5026
|
+
/** Get one domain's detail, verification status, and nameserver records. */
|
|
4694
5027
|
get(domain, signal) {
|
|
4695
5028
|
return this.ctx.transport.getDomain(domain, signal);
|
|
4696
5029
|
}
|
|
5030
|
+
/** Wait up to 50 seconds, then return an explicit resumable outcome. No DNS writes. */
|
|
5031
|
+
wait(domain, options = {}) {
|
|
5032
|
+
return waitForDomain((signal) => this.ctx.transport.getDomain(domain, signal), options);
|
|
5033
|
+
}
|
|
5034
|
+
/** Resume durable updates for this domain using the previous next_cursor as after. */
|
|
5035
|
+
events(domain, params = {}, signal) {
|
|
5036
|
+
return this.ctx.transport.listDomainEvents(domain, params, signal);
|
|
5037
|
+
}
|
|
4697
5038
|
/**
|
|
4698
|
-
*
|
|
4699
|
-
*
|
|
4700
|
-
* the record set / NS instruction.
|
|
5039
|
+
* Add a delegated inbox domain the customer controls. Returns the nameserver
|
|
5040
|
+
* records to publish and never spends money.
|
|
4701
5041
|
*/
|
|
4702
5042
|
onboard(req, signal) {
|
|
4703
5043
|
return this.ctx.transport.onboardDomain(req, signal);
|
|
@@ -4716,6 +5056,46 @@ var Domains = class {
|
|
|
4716
5056
|
return this.ctx.transport.offboardDomain(domain, signal);
|
|
4717
5057
|
}
|
|
4718
5058
|
};
|
|
5059
|
+
var Commerce = class {
|
|
5060
|
+
constructor(ctx) {
|
|
5061
|
+
this.ctx = ctx;
|
|
5062
|
+
}
|
|
5063
|
+
requireIdempotencyKey(value) {
|
|
5064
|
+
if (value.trim().length < 8) {
|
|
5065
|
+
throw new ValidationError({
|
|
5066
|
+
status: 400,
|
|
5067
|
+
code: "bad_request",
|
|
5068
|
+
message: "idempotency_key must be a stable value of at least 8 characters; reuse it for retries of the same intent."
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
/** Quote a domain without purchasing, reserving, or approving it. */
|
|
5073
|
+
quoteDomain(req, signal) {
|
|
5074
|
+
return this.ctx.transport.quoteDomain(req, signal);
|
|
5075
|
+
}
|
|
5076
|
+
/** Create a durable domain-purchase request for human approval. */
|
|
5077
|
+
requestDomainPurchase(req, signal) {
|
|
5078
|
+
this.requireIdempotencyKey(req.idempotency_key);
|
|
5079
|
+
return this.ctx.transport.requestDomainPurchase(req, signal);
|
|
5080
|
+
}
|
|
5081
|
+
/** Create a durable plan-upgrade or downgrade request for human approval. */
|
|
5082
|
+
requestPlanChange(req, signal) {
|
|
5083
|
+
this.requireIdempotencyKey(req.idempotency_key);
|
|
5084
|
+
return this.ctx.transport.requestPlanChange(req, signal);
|
|
5085
|
+
}
|
|
5086
|
+
/** Poll one request's exact blockers, approval URL, and next-action guidance. */
|
|
5087
|
+
get(requestId, signal) {
|
|
5088
|
+
return this.ctx.transport.getCommerceRequest(requestId, signal);
|
|
5089
|
+
}
|
|
5090
|
+
/** Withdraw this agent's request while its durable state still permits cancellation. */
|
|
5091
|
+
cancel(requestId, signal) {
|
|
5092
|
+
return this.ctx.transport.cancelCommerceRequest(requestId, signal);
|
|
5093
|
+
}
|
|
5094
|
+
/** List visible commerce requests using the API's opaque page token. */
|
|
5095
|
+
list(params = {}, signal) {
|
|
5096
|
+
return this.ctx.transport.listCommerceRequests(params, signal);
|
|
5097
|
+
}
|
|
5098
|
+
};
|
|
4719
5099
|
var Reviews = class {
|
|
4720
5100
|
constructor(ctx) {
|
|
4721
5101
|
this.ctx = ctx;
|
|
@@ -4736,7 +5116,7 @@ var Reviews = class {
|
|
|
4736
5116
|
/**
|
|
4737
5117
|
* Get the human's assembled feedback (M5): the diff + comments + decision + the
|
|
4738
5118
|
* rules born from this review. Read it after a rejected/edited nudge to learn what
|
|
4739
|
-
* the human wanted. $0 LLM
|
|
5119
|
+
* the human wanted. $0 LLM: pure assembly on our side.
|
|
4740
5120
|
*/
|
|
4741
5121
|
feedback(reviewId, signal) {
|
|
4742
5122
|
return this.ctx.transport.getReviewFeedback(reviewId, signal);
|
|
@@ -4744,7 +5124,7 @@ var Reviews = class {
|
|
|
4744
5124
|
/**
|
|
4745
5125
|
* Post a chat turn on a review's thread (M5): an agent question to the human
|
|
4746
5126
|
* reviewer; flips in_review -> chatting on the first turn. Idempotent on the
|
|
4747
|
-
* optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM
|
|
5127
|
+
* optional `idempotencyKey` (the `Idempotency-Key` header). $0 LLM: you compose it.
|
|
4748
5128
|
*/
|
|
4749
5129
|
chat(reviewId, req, idempotencyKey, signal) {
|
|
4750
5130
|
return this.ctx.transport.postReviewChat(reviewId, req, idempotencyKey, signal);
|
|
@@ -4752,8 +5132,8 @@ var Reviews = class {
|
|
|
4752
5132
|
/**
|
|
4753
5133
|
* Post a new agent draft under a parent_revision CAS (M5; D17). parent_revision
|
|
4754
5134
|
* must equal the draft's current revision, else a 409 STALE with NO mutation (the
|
|
4755
|
-
* human always wins
|
|
4756
|
-
* in place (revision++) and returns to needs_review. $0 LLM
|
|
5135
|
+
* human always wins: re-read, re-apply, retry). On success the draft is re-rendered
|
|
5136
|
+
* in place (revision++) and returns to needs_review. $0 LLM: you compose the redraft.
|
|
4757
5137
|
*/
|
|
4758
5138
|
revise(reviewId, req, signal) {
|
|
4759
5139
|
return this.ctx.transport.submitRevision(reviewId, req, signal);
|
|
@@ -4772,9 +5152,9 @@ var Reviews = class {
|
|
|
4772
5152
|
* assert "I reviewed this against rules vX and no change is needed", advancing the
|
|
4773
5153
|
* draft's composed_* versions with no new draft, no revision bump, no nudge. A
|
|
4774
5154
|
* born-stale draft re-stamped to the current version becomes current-enough and
|
|
4775
|
-
* releasable on the next reconciliation sweep
|
|
5155
|
+
* releasable on the next reconciliation sweep: the cheap counterpart to revise().
|
|
4776
5156
|
* against_version above the category's current rules-version is 400; a terminal draft
|
|
4777
|
-
* 409s. $0 LLM
|
|
5157
|
+
* 409s. $0 LLM: you judged.
|
|
4778
5158
|
*/
|
|
4779
5159
|
restamp(reviewId, req, signal) {
|
|
4780
5160
|
return this.ctx.transport.restampReview(reviewId, req, signal);
|
|
@@ -4793,13 +5173,13 @@ var Reviews = class {
|
|
|
4793
5173
|
}
|
|
4794
5174
|
/**
|
|
4795
5175
|
* Submit a reviewer decision (M8 Slice B; reviewer_decide, D5/§9). approve/edit → the
|
|
4796
|
-
* PLATFORM
|
|
4797
|
-
* mailbox:send on an inbox it doesn't own
|
|
5176
|
+
* PLATFORM sends with the COMPOSER's credentials (the reviewer NEVER holds
|
|
5177
|
+
* mailbox:send on an inbox it doesn't own: the credential boundary); reject → back to
|
|
4798
5178
|
* the composer (needs_review, hop_count++); escalate → the human queue. revision/
|
|
4799
|
-
* version are the CAS (409 STALE on mismatch, NO mutation
|
|
5179
|
+
* version are the CAS (409 STALE on mismatch, NO mutation: the human always wins,
|
|
4800
5180
|
* D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline)
|
|
4801
|
-
* FORCE a reject to the human regardless of intent
|
|
4802
|
-
* LLM
|
|
5181
|
+
* FORCE a reject to the human regardless of intent: `forced_by_breaker` names it. $0
|
|
5182
|
+
* LLM: you judged; we route, send, and enforce the breakers.
|
|
4803
5183
|
*/
|
|
4804
5184
|
decide(reviewId, req, signal) {
|
|
4805
5185
|
return this.ctx.transport.reviewerDecide(reviewId, req, signal);
|
|
@@ -4838,14 +5218,14 @@ var Categories = class {
|
|
|
4838
5218
|
propose(req, signal) {
|
|
4839
5219
|
return this.ctx.transport.proposeCategory(req, signal);
|
|
4840
5220
|
}
|
|
4841
|
-
/** Rename / re-describe a category
|
|
5221
|
+
/** Rename / re-describe a category: metadata only (D10). */
|
|
4842
5222
|
update(categoryId, req, signal) {
|
|
4843
5223
|
return this.ctx.transport.updateCategory(categoryId, req, signal);
|
|
4844
5224
|
}
|
|
4845
5225
|
/**
|
|
4846
5226
|
* Read the effective risk dial (D4/D12): the account default + every category's
|
|
4847
5227
|
* overrides (each with its resolved effective value; null override = inherit).
|
|
4848
|
-
* Read-only
|
|
5228
|
+
* Read-only: agents read but NEVER flip the dial; setting it is a human (console)
|
|
4849
5229
|
* action (D16).
|
|
4850
5230
|
*/
|
|
4851
5231
|
riskDial(signal) {
|
|
@@ -4861,7 +5241,7 @@ var Categories = class {
|
|
|
4861
5241
|
}
|
|
4862
5242
|
/**
|
|
4863
5243
|
* Propose graduating a category (D16/D6): RECORDS the request (durable evidence) and
|
|
4864
|
-
* returns the current gate status. It does NOT change the category state
|
|
5244
|
+
* returns the current gate status. It does NOT change the category state: flipping
|
|
4865
5245
|
* the bit is a human (console) action; an agent only proposes.
|
|
4866
5246
|
*/
|
|
4867
5247
|
proposeGraduation(categoryId, req = {}, signal) {
|
|
@@ -4870,7 +5250,7 @@ var Categories = class {
|
|
|
4870
5250
|
/**
|
|
4871
5251
|
* Read the D19/§8 backlog-reconciliation status: how many of the category's QUEUED
|
|
4872
5252
|
* drafts are stale vs current-enough against the current rules-version (a pure
|
|
4873
|
-
* integer compare, $0 LLM). Read-only
|
|
5253
|
+
* integer compare, $0 LLM). Read-only: you READ the picture; the human (console
|
|
4874
5254
|
* scan-backlog) or the graduate/rule-change hooks TRIGGER the actual reconciliation
|
|
4875
5255
|
* sweep that releases current-enough drafts and nudges stale ones to redraft.
|
|
4876
5256
|
*/
|
|
@@ -4901,7 +5281,7 @@ var Rules = class {
|
|
|
4901
5281
|
* Save / edit a rule (append-only by supersession; D11). An agent-plane save is
|
|
4902
5282
|
* ALWAYS project-layer: the saved rule's `rule_layer` is `project`, bound to the
|
|
4903
5283
|
* key's project. Agents cannot author org-layer / house-style (`rule_layer:"org"`)
|
|
4904
|
-
* rules in v1
|
|
5284
|
+
* rules in v1: that is a console/admin action.
|
|
4905
5285
|
*/
|
|
4906
5286
|
save(req, signal) {
|
|
4907
5287
|
return this.ctx.transport.saveRule(req, signal);
|
|
@@ -4910,7 +5290,7 @@ var Rules = class {
|
|
|
4910
5290
|
promote(ruleId, toScope, signal) {
|
|
4911
5291
|
return this.ctx.transport.promoteRule(ruleId, toScope, signal);
|
|
4912
5292
|
}
|
|
4913
|
-
/** Retire a rule
|
|
5293
|
+
/** Retire a rule: soft delete; the history survives as training data. */
|
|
4914
5294
|
retire(ruleId, signal) {
|
|
4915
5295
|
return this.ctx.transport.retireRule(ruleId, signal);
|
|
4916
5296
|
}
|
|
@@ -4918,7 +5298,7 @@ var Rules = class {
|
|
|
4918
5298
|
audit(params = {}, signal) {
|
|
4919
5299
|
return this.ctx.transport.getRuleAudit(params, signal);
|
|
4920
5300
|
}
|
|
4921
|
-
/** Undo a rule change by its audit-row id (udo_…)
|
|
5301
|
+
/** Undo a rule change by its audit-row id (udo_…): restore the prior version. */
|
|
4922
5302
|
undo(udoId, signal) {
|
|
4923
5303
|
return this.ctx.transport.undoRuleChange(udoId, signal);
|
|
4924
5304
|
}
|
|
@@ -4975,26 +5355,31 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
4975
5355
|
this.contactLists = new ContactLists(ctx);
|
|
4976
5356
|
this.suppressions = new Suppressions(ctx);
|
|
4977
5357
|
this.domains = new Domains(ctx);
|
|
5358
|
+
this.commerce = new Commerce(ctx);
|
|
4978
5359
|
this.reviews = new Reviews(ctx);
|
|
4979
5360
|
this.categories = new Categories(ctx);
|
|
4980
5361
|
this.rules = new Rules(ctx);
|
|
4981
5362
|
this.projects = new Projects(ctx);
|
|
4982
5363
|
}
|
|
4983
5364
|
/**
|
|
4984
|
-
* Redeem an enrollment token (`pk_enroll_...`) and
|
|
5365
|
+
* Redeem an enrollment token (`pk_enroll_...`) and issue a scoped agent key.
|
|
4985
5366
|
*
|
|
4986
5367
|
* Idempotent on `agent_handle`: redeeming twice with the same handle returns the same agent.
|
|
4987
|
-
* Returns the raw `EnrollResponse`
|
|
5368
|
+
* Returns the raw `EnrollResponse` - to immediately use the issued key, prefer
|
|
4988
5369
|
* {@link ExtrovertClient.enrolled}.
|
|
4989
5370
|
*/
|
|
4990
5371
|
enroll(req, signal) {
|
|
4991
5372
|
return this.transport.enroll(req, signal);
|
|
4992
5373
|
}
|
|
4993
5374
|
/**
|
|
4994
|
-
*
|
|
4995
|
-
*
|
|
4996
|
-
*
|
|
4997
|
-
*
|
|
5375
|
+
* Request a free account in one unauthenticated call. When free signup is
|
|
5376
|
+
* enabled, this provisions a tenant plus a first inbox and returns a
|
|
5377
|
+
* verification-only agent key. That key can only call {@link verify}; it cannot
|
|
5378
|
+
* read or send mail. A one-time code is emailed to `human_email`. Call
|
|
5379
|
+
* {@link verify} with the code to activate the account and receive full scopes.
|
|
5380
|
+
* Idempotent on `human_email`: re-calling rotates the key and resends the code.
|
|
5381
|
+
* When free signup is paused, this throws an `ApiError` with status 403 and
|
|
5382
|
+
* code `signup_disabled` without creating account state.
|
|
4998
5383
|
*/
|
|
4999
5384
|
signUp(req, signal) {
|
|
5000
5385
|
return this.transport.signUp(req, signal);
|
|
@@ -5004,6 +5389,8 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5004
5389
|
* once). Must be called with the limited key from {@link signUp} as the bearer.
|
|
5005
5390
|
* The result repeats the ready inbox address and includes MCP-first list/read/wait
|
|
5006
5391
|
* calls; SDK callers can pass `address` directly to `inboxes` and `messages`.
|
|
5392
|
+
* Pending verification is also fail-closed with 403 `signup_disabled` while
|
|
5393
|
+
* free signup is paused.
|
|
5007
5394
|
*/
|
|
5008
5395
|
verify(req, signal) {
|
|
5009
5396
|
return this.transport.verify(req, signal);
|
|
@@ -5013,7 +5400,7 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5013
5400
|
return this.transport.whoami(signal);
|
|
5014
5401
|
}
|
|
5015
5402
|
/**
|
|
5016
|
-
* Poll the status of an async job (`GET /v1/jobs/{job_id}`)
|
|
5403
|
+
* Poll the status of an async job (`GET /v1/jobs/{job_id}`) - currently only
|
|
5017
5404
|
* the domain-offboard teardown started by {@link Domains.offboard} enqueues
|
|
5018
5405
|
* one. `status` is terminal on succeeded/failed/cancelled; keep polling
|
|
5019
5406
|
* otherwise. An unknown or foreign job id is a {@link NotFoundError}.
|
|
@@ -5022,8 +5409,8 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5022
5409
|
return this.transport.getJob(jobId, signal);
|
|
5023
5410
|
}
|
|
5024
5411
|
/**
|
|
5025
|
-
* Redeem an enrollment token and return a *new* client already authenticated with the
|
|
5026
|
-
* agent key
|
|
5412
|
+
* Redeem an enrollment token and return a *new* client already authenticated with the issued
|
|
5413
|
+
* agent key - the natural "redeem then act" flow for an agent.
|
|
5027
5414
|
*
|
|
5028
5415
|
* ```ts
|
|
5029
5416
|
* const bootstrap = new Extrovert({ apiKey: enrollmentToken });
|
|
@@ -5044,7 +5431,7 @@ var ExtrovertClient = class _ExtrovertClient {
|
|
|
5044
5431
|
return { client, enrollment };
|
|
5045
5432
|
}
|
|
5046
5433
|
/**
|
|
5047
|
-
* Get an ergonomic handle to an existing inbox by address
|
|
5434
|
+
* Get an ergonomic handle to an existing inbox by address - without an extra round-trip. Use this
|
|
5048
5435
|
* when you already know the address (e.g. from a previous create) and want to send/wait/reply.
|
|
5049
5436
|
* Call {@link InboxHandle.refresh} to load the full record.
|
|
5050
5437
|
*/
|
|
@@ -5171,14 +5558,14 @@ async function signWebhook(secret, body, timestampSeconds) {
|
|
|
5171
5558
|
}
|
|
5172
5559
|
|
|
5173
5560
|
// src/contract.ts
|
|
5174
|
-
var CONTRACT_VERSION = "0.1.0-pre.
|
|
5561
|
+
var CONTRACT_VERSION = "0.1.0-pre.7";
|
|
5175
5562
|
var CONTRACT_MANIFEST = {
|
|
5176
5563
|
name: "extrovert.review-loop",
|
|
5177
5564
|
version: CONTRACT_VERSION,
|
|
5178
5565
|
stability: "provisional",
|
|
5179
5566
|
kind: "sdk+skill-contract",
|
|
5180
5567
|
spec_ref: "hitl-spec.md#11",
|
|
5181
|
-
// §11 core
|
|
5568
|
+
// §11 core - the five canonical example shapes.
|
|
5182
5569
|
core_shapes: ["ReviewIntent", "ReviewFeedback", "DiffJson", "Rule", "ReviewEvent"],
|
|
5183
5570
|
// The FULL published surface (the §11 core plus the rest of M1–M8). Adding a
|
|
5184
5571
|
// name here without a matching re-export (or vice-versa) breaks the drift test.
|
|
@@ -5229,7 +5616,16 @@ var CONTRACT_MANIFEST = {
|
|
|
5229
5616
|
"ReviewerAction",
|
|
5230
5617
|
"ReviewDecisionContext",
|
|
5231
5618
|
"ReviewerDecisionRequest",
|
|
5232
|
-
"ReviewerDecisionResult"
|
|
5619
|
+
"ReviewerDecisionResult",
|
|
5620
|
+
// agent commerce request plane
|
|
5621
|
+
"CommerceBlocker",
|
|
5622
|
+
"QuoteDomainRequest",
|
|
5623
|
+
"DomainQuote",
|
|
5624
|
+
"CommerceRequestKind",
|
|
5625
|
+
"RequestDomainPurchaseRequest",
|
|
5626
|
+
"RequestPlanChangeRequest",
|
|
5627
|
+
"ListCommerceRequestsParams",
|
|
5628
|
+
"CommerceRequest"
|
|
5233
5629
|
],
|
|
5234
5630
|
// The complete Review Loop behavior lives in the send skill; writing-rule
|
|
5235
5631
|
// governance remains independently installable and part of this contract.
|
|
@@ -5245,6 +5641,7 @@ exports.CONTRACT_MANIFEST = CONTRACT_MANIFEST;
|
|
|
5245
5641
|
exports.CONTRACT_VERSION = CONTRACT_VERSION;
|
|
5246
5642
|
exports.CURRENT_API_VERSION = CURRENT_API_VERSION;
|
|
5247
5643
|
exports.Categories = Categories;
|
|
5644
|
+
exports.Commerce = Commerce;
|
|
5248
5645
|
exports.ConflictError = ConflictError;
|
|
5249
5646
|
exports.ConnectionError = ConnectionError;
|
|
5250
5647
|
exports.ContactLists = ContactLists;
|