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