@agentcomms/resend 0.15.1 → 0.16.0
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 +4 -0
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +301 -7
- package/dist/index.mjs +202 -3
- package/dist/{send-BcM5UKMt.mjs → send-BwyJM23a.mjs} +760 -115
- package/dist/{server-EV5f6d28.mjs → server-BFAo5PpV.mjs} +1 -1
- package/dist/{stdio-entry-Ea4hMItp.mjs → stdio-entry-BGq1Q5V_.mjs} +1 -1
- package/package.json +13 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { S as union, b as strictObject, c as boolean, h as number, i as _enum, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-USuVU_sk.mjs";
|
|
1
|
+
import { S as union, b as strictObject, c as boolean, h as number, i as _enum, l as discriminatedUnion, m as looseObject, n as datetime, p as literal$1, s as array, x as string, y as record } from "./iso-USuVU_sk.mjs";
|
|
2
2
|
import { access, chmod, constants, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, unlink, utimes } from "node:fs/promises";
|
|
3
3
|
import path, { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { domainToASCII, fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -12,7 +12,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/version.ts
|
|
14
14
|
/** The package version, read from package.json at build time. */
|
|
15
|
-
const VERSION$1 = "0.
|
|
15
|
+
const VERSION$1 = "0.16.0";
|
|
16
16
|
//#endregion
|
|
17
17
|
//#region src/api/routes.ts
|
|
18
18
|
/**
|
|
@@ -201,7 +201,7 @@ const RESEND_CALLER = Object.freeze({
|
|
|
201
201
|
packageName: PACKAGE_NAME
|
|
202
202
|
});
|
|
203
203
|
//#endregion
|
|
204
|
-
//#region ../core/dist/update-check-
|
|
204
|
+
//#region ../core/dist/update-check-23Ii90fQ.mjs
|
|
205
205
|
/**
|
|
206
206
|
* Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
|
|
207
207
|
* optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
|
|
@@ -317,6 +317,11 @@ const ERROR_REGISTRY = {
|
|
|
317
317
|
retryable: true,
|
|
318
318
|
summary: "the mail provider is unavailable"
|
|
319
319
|
},
|
|
320
|
+
SOURCE_UNAVAILABLE: {
|
|
321
|
+
exit: EXIT_CODES.UNAVAILABLE,
|
|
322
|
+
retryable: false,
|
|
323
|
+
summary: "the selected local event source is unavailable"
|
|
324
|
+
},
|
|
320
325
|
SECRET_STORE_UNAVAILABLE: {
|
|
321
326
|
exit: EXIT_CODES.UNAVAILABLE,
|
|
322
327
|
retryable: false,
|
|
@@ -347,6 +352,11 @@ const ERROR_REGISTRY = {
|
|
|
347
352
|
retryable: false,
|
|
348
353
|
summary: "the inbox was not granted this permission"
|
|
349
354
|
},
|
|
355
|
+
WHATSAPP_VISIBILITY_SEAM_REQUIRED: {
|
|
356
|
+
exit: EXIT_CODES.CONFIG,
|
|
357
|
+
retryable: false,
|
|
358
|
+
summary: "the WhatsApp event visibility fence is unavailable"
|
|
359
|
+
},
|
|
350
360
|
CONFIG: {
|
|
351
361
|
exit: EXIT_CODES.CONFIG,
|
|
352
362
|
retryable: false,
|
|
@@ -843,12 +853,29 @@ function dedupeBy(items, key) {
|
|
|
843
853
|
}
|
|
844
854
|
return out;
|
|
845
855
|
}
|
|
856
|
+
const ORIGINS$1 = ["event", "read"];
|
|
857
|
+
function sortedOrigins(origins) {
|
|
858
|
+
const known = new Set(origins);
|
|
859
|
+
return ORIGINS$1.filter((origin) => known.has(origin));
|
|
860
|
+
}
|
|
861
|
+
function originEntry(entry) {
|
|
862
|
+
if (entry === void 0) return ["read"];
|
|
863
|
+
const known = Array.isArray(entry.origins) ? entry.origins.filter((origin) => origin === "event" || origin === "read") : [];
|
|
864
|
+
return known.length === 0 ? ["read"] : sortedOrigins(known);
|
|
865
|
+
}
|
|
866
|
+
function isNewer(at, than) {
|
|
867
|
+
const left = new Date(at).getTime();
|
|
868
|
+
const right = new Date(than).getTime();
|
|
869
|
+
return Number.isFinite(left) && Number.isFinite(right) && left > right;
|
|
870
|
+
}
|
|
846
871
|
var TaintStore = class {
|
|
847
872
|
directory;
|
|
848
873
|
#now;
|
|
849
|
-
|
|
874
|
+
#hooks;
|
|
875
|
+
constructor(stateDir, now = () => /* @__PURE__ */ new Date(), hooks = {}) {
|
|
850
876
|
this.directory = join(stateDir, "taint");
|
|
851
877
|
this.#now = now;
|
|
878
|
+
this.#hooks = hooks;
|
|
852
879
|
}
|
|
853
880
|
get #path() {
|
|
854
881
|
return join(this.directory, "taint.json");
|
|
@@ -856,6 +883,9 @@ var TaintStore = class {
|
|
|
856
883
|
get #handlesPath() {
|
|
857
884
|
return join(this.directory, "handles.json");
|
|
858
885
|
}
|
|
886
|
+
get #originsPath() {
|
|
887
|
+
return join(this.directory, "origins.json");
|
|
888
|
+
}
|
|
859
889
|
async #readJson(path, empty) {
|
|
860
890
|
try {
|
|
861
891
|
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
@@ -881,6 +911,13 @@ var TaintStore = class {
|
|
|
881
911
|
async #readHandles() {
|
|
882
912
|
return this.#readJson(this.#handlesPath, () => ({ handles: {} }));
|
|
883
913
|
}
|
|
914
|
+
async #readOrigins() {
|
|
915
|
+
return this.#readJson(this.#originsPath, () => ({
|
|
916
|
+
addresses: {},
|
|
917
|
+
domains: {},
|
|
918
|
+
handles: {}
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
884
921
|
#prune(file) {
|
|
885
922
|
const cutoff = this.#now().getTime() - TAINT_WINDOW_MS;
|
|
886
923
|
const keep = (map) => {
|
|
@@ -904,6 +941,35 @@ var TaintStore = class {
|
|
|
904
941
|
handles: Object.fromEntries(fresh.slice(0, MAX_ENTRIES))
|
|
905
942
|
};
|
|
906
943
|
}
|
|
944
|
+
#mergeOrigins(origins, addresses, domains, handles, touched, at) {
|
|
945
|
+
const merge = (sidecar, base, observed) => {
|
|
946
|
+
const merged = {};
|
|
947
|
+
for (const [key, entry] of Object.entries(sidecar)) {
|
|
948
|
+
const baseEntry = base[key];
|
|
949
|
+
if (baseEntry === void 0) continue;
|
|
950
|
+
const mergedOrigins = new Set(originEntry(entry));
|
|
951
|
+
if (isNewer(baseEntry.at, entry.at)) mergedOrigins.add("read");
|
|
952
|
+
merged[key] = {
|
|
953
|
+
at: isNewer(baseEntry.at, entry.at) ? baseEntry.at : entry.at,
|
|
954
|
+
origins: sortedOrigins(mergedOrigins)
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
for (const [key, observedOrigins] of observed) {
|
|
958
|
+
const existing = merged[key];
|
|
959
|
+
merged[key] = {
|
|
960
|
+
at,
|
|
961
|
+
origins: sortedOrigins([...existing?.origins ?? [], ...observedOrigins])
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return merged;
|
|
965
|
+
};
|
|
966
|
+
return {
|
|
967
|
+
...origins,
|
|
968
|
+
addresses: merge(origins.addresses, addresses, touched.addresses),
|
|
969
|
+
domains: merge(origins.domains, domains, touched.domains),
|
|
970
|
+
handles: merge(origins.handles, handles, touched.handles)
|
|
971
|
+
};
|
|
972
|
+
}
|
|
907
973
|
/**
|
|
908
974
|
* Records observations. Throws if it cannot — callers must fail the read rather than return content whose taint
|
|
909
975
|
* was not recorded.
|
|
@@ -922,7 +988,17 @@ var TaintStore = class {
|
|
|
922
988
|
})).filter((o) => o.address.includes("@") && !own.has(o.address) && !internal.has(domainOf(o.address) ?? "")).sort((a, b) => a.source === b.source ? 0 : a.source === "header" ? -1 : 1), (o) => o.address).slice(0, MAX_PER_MESSAGE);
|
|
923
989
|
if (kept.length === 0 && keptHandles.length === 0) return;
|
|
924
990
|
const at = this.#now().toISOString();
|
|
925
|
-
const
|
|
991
|
+
const touched = {
|
|
992
|
+
addresses: /* @__PURE__ */ new Map(),
|
|
993
|
+
domains: /* @__PURE__ */ new Map(),
|
|
994
|
+
handles: /* @__PURE__ */ new Map()
|
|
995
|
+
};
|
|
996
|
+
const rememberOrigin = (map, key, origin) => {
|
|
997
|
+
const origins = touched[map].get(key) ?? /* @__PURE__ */ new Set();
|
|
998
|
+
origins.add(origin ?? "read");
|
|
999
|
+
touched[map].set(key, origins);
|
|
1000
|
+
};
|
|
1001
|
+
const touch = (map, key, o, originMap) => {
|
|
926
1002
|
const existing = map[key];
|
|
927
1003
|
const inboxIds = [.../* @__PURE__ */ new Set([...existing?.inboxIds ?? [], o.inboxId])];
|
|
928
1004
|
const source = existing?.source === "header" ? "header" : o.source;
|
|
@@ -931,27 +1007,38 @@ var TaintStore = class {
|
|
|
931
1007
|
source,
|
|
932
1008
|
inboxIds
|
|
933
1009
|
};
|
|
1010
|
+
rememberOrigin(originMap, key, o.origin);
|
|
934
1011
|
};
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1012
|
+
const commit = async () => {
|
|
1013
|
+
const taint = this.#prune(await this.#read());
|
|
1014
|
+
const handles = this.#pruneHandles(await this.#readHandles());
|
|
1015
|
+
if (kept.length > 0) for (const o of kept) {
|
|
1016
|
+
touch(taint.addresses, o.address, o, "addresses");
|
|
1017
|
+
const domain = domainOf(o.address);
|
|
1018
|
+
if (domain && !PUBLIC_MAILBOX_DOMAINS.has(domain)) touch(taint.domains, domain, o, "domains");
|
|
1019
|
+
}
|
|
1020
|
+
if (keptHandles.length > 0) for (const o of keptHandles) touch(handles.handles, o.key, o, "handles");
|
|
1021
|
+
const finalTaint = this.#prune(taint);
|
|
1022
|
+
const finalHandles = this.#pruneHandles(handles);
|
|
1023
|
+
const origins = this.#mergeOrigins(await this.#readOrigins(), finalTaint.addresses, finalTaint.domains, finalHandles.handles, touched, at);
|
|
1024
|
+
await writeFileAtomic(this.#originsPath, JSON.stringify(origins));
|
|
1025
|
+
await this.#hooks.afterOriginsCommit?.();
|
|
1026
|
+
if (kept.length > 0) await writeFileAtomic(this.#path, JSON.stringify(finalTaint));
|
|
1027
|
+
if (keptHandles.length > 0) await writeFileAtomic(this.#handlesPath, JSON.stringify(finalHandles));
|
|
1028
|
+
};
|
|
1029
|
+
const withBaseLocks = async () => {
|
|
1030
|
+
await withFileLock(`${this.#path}.lock`, async () => {
|
|
1031
|
+
this.#hooks.onLockAcquired?.("taint");
|
|
1032
|
+
await withFileLock(`${this.#handlesPath}.lock`, async () => {
|
|
1033
|
+
this.#hooks.onLockAcquired?.("handles");
|
|
1034
|
+
await commit();
|
|
1035
|
+
});
|
|
953
1036
|
});
|
|
954
|
-
}
|
|
1037
|
+
};
|
|
1038
|
+
await withFileLock(`${this.#originsPath}.lock`, async () => {
|
|
1039
|
+
this.#hooks.onLockAcquired?.("origins");
|
|
1040
|
+
await withBaseLocks();
|
|
1041
|
+
});
|
|
955
1042
|
}
|
|
956
1043
|
/**
|
|
957
1044
|
* Whether an address, or its (non-public) domain, was seen in email content in the window — from any inbox — and the
|
|
@@ -959,20 +1046,22 @@ var TaintStore = class {
|
|
|
959
1046
|
*/
|
|
960
1047
|
async check(address) {
|
|
961
1048
|
const file = this.#prune(await this.#read());
|
|
1049
|
+
const origins = await this.#readOrigins();
|
|
962
1050
|
const canonical = canonicalAddress(address);
|
|
963
1051
|
const domain = domainOf(canonical);
|
|
964
1052
|
const exact = Object.hasOwn(file.addresses, canonical) ? file.addresses[canonical] : void 0;
|
|
965
1053
|
const byDomain = domain !== null && !PUBLIC_MAILBOX_DOMAINS.has(domain) && Object.hasOwn(file.domains, domain) ? file.domains[domain] : void 0;
|
|
966
|
-
const aggregate = (entry) => ({
|
|
1054
|
+
const aggregate = (entry, origin) => ({
|
|
967
1055
|
at: entry.at,
|
|
968
1056
|
source: entry.source,
|
|
969
|
-
inboxIds: [...entry.inboxIds]
|
|
1057
|
+
inboxIds: [...entry.inboxIds],
|
|
1058
|
+
origins: sortedOrigins([...originEntry(origin), ...origin !== void 0 && isNewer(entry.at, origin.at) ? ["read"] : []])
|
|
970
1059
|
});
|
|
971
1060
|
return {
|
|
972
1061
|
address: exact !== void 0,
|
|
973
1062
|
domain: byDomain !== void 0,
|
|
974
|
-
...exact === void 0 ? {} : { addressSeen: aggregate(exact) },
|
|
975
|
-
...byDomain === void 0 ? {} : { domainSeen: aggregate(byDomain) }
|
|
1063
|
+
...exact === void 0 ? {} : { addressSeen: aggregate(exact, origins.addresses[canonical]) },
|
|
1064
|
+
...byDomain === void 0 ? {} : { domainSeen: aggregate(byDomain, origins.domains[domain ?? ""]) }
|
|
976
1065
|
};
|
|
977
1066
|
}
|
|
978
1067
|
/**
|
|
@@ -985,6 +1074,15 @@ var TaintStore = class {
|
|
|
985
1074
|
const file = this.#pruneHandles(await this.#readHandles());
|
|
986
1075
|
return canonicalHandle(handle) in file.handles;
|
|
987
1076
|
}
|
|
1077
|
+
/** The merged provenance of a retained handle, or no origins when it is not in the taint window. */
|
|
1078
|
+
async checkHandleOrigins(handle) {
|
|
1079
|
+
const file = this.#pruneHandles(await this.#readHandles());
|
|
1080
|
+
const key = canonicalHandle(handle);
|
|
1081
|
+
const entry = file.handles[key];
|
|
1082
|
+
if (entry === void 0) return [];
|
|
1083
|
+
const sidecar = (await this.#readOrigins()).handles[key];
|
|
1084
|
+
return sortedOrigins([...originEntry(sidecar), ...sidecar !== void 0 && isNewer(entry.at, sidecar.at) ? ["read"] : []]);
|
|
1085
|
+
}
|
|
988
1086
|
};
|
|
989
1087
|
/**
|
|
990
1088
|
* The context every read path uses to put sender-controlled content into a result. It wraps strings in the untrusted
|
|
@@ -1000,21 +1098,23 @@ var TaintCollector = class {
|
|
|
1000
1098
|
this.#messageId = messageId;
|
|
1001
1099
|
}
|
|
1002
1100
|
/** Scans free text (bodies, subjects, snippets, attachment text) for addresses. */
|
|
1003
|
-
observeText(text) {
|
|
1101
|
+
observeText(text, origin = "read") {
|
|
1004
1102
|
for (const address of extractAddresses(text)) this.#observations.push({
|
|
1005
1103
|
address,
|
|
1006
1104
|
source: "body",
|
|
1105
|
+
origin,
|
|
1007
1106
|
inboxId: this.#inboxId,
|
|
1008
1107
|
messageId: this.#messageId
|
|
1009
1108
|
});
|
|
1010
1109
|
}
|
|
1011
1110
|
/** Records parsed header addresses (From, Reply-To, Sender, To, Cc). */
|
|
1012
|
-
observeHeaders(addresses) {
|
|
1111
|
+
observeHeaders(addresses, origin = "read") {
|
|
1013
1112
|
for (const address of addresses) {
|
|
1014
1113
|
const canonical = canonicalAddress(address);
|
|
1015
1114
|
if (canonical.includes("@")) this.#observations.push({
|
|
1016
1115
|
address: canonical,
|
|
1017
1116
|
source: "header",
|
|
1117
|
+
origin,
|
|
1018
1118
|
inboxId: this.#inboxId,
|
|
1019
1119
|
messageId: this.#messageId
|
|
1020
1120
|
});
|
|
@@ -1027,10 +1127,11 @@ var TaintCollector = class {
|
|
|
1027
1127
|
* second place to keep it correct. What core insists on is that the caller hands over ids rather than the display
|
|
1028
1128
|
* names beside them, which the account being named can change at any time.
|
|
1029
1129
|
*/
|
|
1030
|
-
observeHandles(handles, source = "body") {
|
|
1130
|
+
observeHandles(handles, source = "body", origin = "read") {
|
|
1031
1131
|
for (const handle of handles) this.#handles.push({
|
|
1032
1132
|
handle,
|
|
1033
1133
|
source,
|
|
1134
|
+
origin,
|
|
1034
1135
|
inboxId: this.#inboxId,
|
|
1035
1136
|
messageId: this.#messageId
|
|
1036
1137
|
});
|
|
@@ -1127,7 +1228,19 @@ const CHANNEL_SNAPSHOT = [
|
|
|
1127
1228
|
prefix: "gmail-",
|
|
1128
1229
|
contract: "skills/_shared/contract-gmail.md"
|
|
1129
1230
|
},
|
|
1130
|
-
approvalGrouping: "draft"
|
|
1231
|
+
approvalGrouping: "draft",
|
|
1232
|
+
events: {
|
|
1233
|
+
types: [
|
|
1234
|
+
"gmail.message.received",
|
|
1235
|
+
"gmail.message.sent",
|
|
1236
|
+
"gmail.message.labelled"
|
|
1237
|
+
],
|
|
1238
|
+
minimumIntervalMs: 6e4,
|
|
1239
|
+
access: {
|
|
1240
|
+
kind: "oauth-user",
|
|
1241
|
+
requiredScopes: ["https://www.googleapis.com/auth/gmail.readonly"]
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1131
1244
|
}
|
|
1132
1245
|
},
|
|
1133
1246
|
{
|
|
@@ -1167,7 +1280,12 @@ const CHANNEL_SNAPSHOT = [
|
|
|
1167
1280
|
prefix: "resend-",
|
|
1168
1281
|
contract: "skills/_shared/contract-resend.md"
|
|
1169
1282
|
},
|
|
1170
|
-
approvalGrouping: "draft"
|
|
1283
|
+
approvalGrouping: "draft",
|
|
1284
|
+
events: {
|
|
1285
|
+
types: ["resend.email.received", "resend.email.status_changed"],
|
|
1286
|
+
minimumIntervalMs: 6e4,
|
|
1287
|
+
access: { kind: "resend-full-access" }
|
|
1288
|
+
}
|
|
1171
1289
|
}
|
|
1172
1290
|
},
|
|
1173
1291
|
{
|
|
@@ -1207,7 +1325,20 @@ const CHANNEL_SNAPSHOT = [
|
|
|
1207
1325
|
prefix: "slack-",
|
|
1208
1326
|
contract: "skills/_shared/contract-slack.md"
|
|
1209
1327
|
},
|
|
1210
|
-
approvalGrouping: "draft-revision-digest"
|
|
1328
|
+
approvalGrouping: "draft-revision-digest",
|
|
1329
|
+
events: {
|
|
1330
|
+
types: ["slack.message.posted"],
|
|
1331
|
+
minimumIntervalMs: 6e4,
|
|
1332
|
+
access: {
|
|
1333
|
+
kind: "oauth-user",
|
|
1334
|
+
requiredScopes: [
|
|
1335
|
+
"channels:history",
|
|
1336
|
+
"groups:history",
|
|
1337
|
+
"im:history",
|
|
1338
|
+
"mpim:history"
|
|
1339
|
+
]
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1211
1342
|
}
|
|
1212
1343
|
},
|
|
1213
1344
|
{
|
|
@@ -1246,10 +1377,76 @@ const CHANNEL_SNAPSHOT = [
|
|
|
1246
1377
|
skills: {
|
|
1247
1378
|
prefix: "whatsapp-",
|
|
1248
1379
|
contract: "skills/_shared/contract-whatsapp.md"
|
|
1380
|
+
},
|
|
1381
|
+
events: {
|
|
1382
|
+
types: ["whatsapp.message.received"],
|
|
1383
|
+
minimumIntervalMs: 6e4,
|
|
1384
|
+
access: { kind: "local-store" }
|
|
1249
1385
|
}
|
|
1250
1386
|
}
|
|
1251
1387
|
}
|
|
1252
1388
|
];
|
|
1389
|
+
const DISCLOSURE_VERSION_KINDS = [
|
|
1390
|
+
"rule",
|
|
1391
|
+
"target",
|
|
1392
|
+
"subscriber",
|
|
1393
|
+
"judge",
|
|
1394
|
+
"judge-budget",
|
|
1395
|
+
"judge-kind"
|
|
1396
|
+
];
|
|
1397
|
+
const DISCLOSURE_ACTIVATION_KINDS = [
|
|
1398
|
+
"rule",
|
|
1399
|
+
"judge-budget",
|
|
1400
|
+
"judge-kind",
|
|
1401
|
+
"enable-all"
|
|
1402
|
+
];
|
|
1403
|
+
const HEX64$1 = /^[0-9a-f]{64}$/;
|
|
1404
|
+
function disclosureFailure() {
|
|
1405
|
+
throw new CommsError("BAD_DATA", "a disclosure binding is not in its canonical activation shape");
|
|
1406
|
+
}
|
|
1407
|
+
function compareDisclosureVersions(left, right) {
|
|
1408
|
+
const compareText = (one, two) => one < two ? -1 : one > two ? 1 : 0;
|
|
1409
|
+
return compareText(left.kind, right.kind) || compareText(left.id, right.id) || left.version - right.version;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Validates and copies a disclosure binding without reordering it. A caller must provide the
|
|
1413
|
+
* daemon-derived canonical list, already sorted by (kind, id, version), so a different document
|
|
1414
|
+
* cannot become authorised by a convenient normalisation here.
|
|
1415
|
+
*/
|
|
1416
|
+
function canonicalDisclosureBinding(binding) {
|
|
1417
|
+
if (!HEX64$1.test(binding.digest) || typeof binding.activationIntentId !== "string" || binding.activationIntentId.length === 0 || !DISCLOSURE_ACTIVATION_KINDS.includes(binding.activationKind) || !Array.isArray(binding.versions)) return disclosureFailure();
|
|
1418
|
+
const versions = binding.versions.map((entry) => {
|
|
1419
|
+
if (!DISCLOSURE_VERSION_KINDS.includes(entry.kind) || typeof entry.id !== "string" || entry.id.length === 0 || !Number.isSafeInteger(entry.version) || entry.version < 1) return disclosureFailure();
|
|
1420
|
+
return {
|
|
1421
|
+
kind: entry.kind,
|
|
1422
|
+
id: entry.id,
|
|
1423
|
+
version: entry.version
|
|
1424
|
+
};
|
|
1425
|
+
});
|
|
1426
|
+
for (let index = 1; index < versions.length; index += 1) if (compareDisclosureVersions(versions[index - 1], versions[index]) >= 0) return disclosureFailure();
|
|
1427
|
+
const kinds = new Set(versions.map((entry) => entry.kind));
|
|
1428
|
+
if (binding.activationKind === "rule") {
|
|
1429
|
+
if (!kinds.has("rule") || kinds.has("judge-budget") || kinds.has("judge-kind")) return disclosureFailure();
|
|
1430
|
+
} else if (binding.activationKind === "judge-budget") {
|
|
1431
|
+
if (versions.length !== 1 || versions[0]?.kind !== "judge-budget") return disclosureFailure();
|
|
1432
|
+
} else if (binding.activationKind === "judge-kind") {
|
|
1433
|
+
if (versions.length !== 1 || versions[0]?.kind !== "judge-kind") return disclosureFailure();
|
|
1434
|
+
} else if (versions.some((entry) => entry.kind !== "rule")) return disclosureFailure();
|
|
1435
|
+
return {
|
|
1436
|
+
digest: binding.digest,
|
|
1437
|
+
activationIntentId: binding.activationIntentId,
|
|
1438
|
+
activationKind: binding.activationKind,
|
|
1439
|
+
versions
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/** Exact canonical equality for each re-check at approval and claim. */
|
|
1443
|
+
function sameDisclosureBinding(left, right) {
|
|
1444
|
+
try {
|
|
1445
|
+
return canonicalJson$1(canonicalDisclosureBinding(left)) === canonicalJson$1(canonicalDisclosureBinding(right));
|
|
1446
|
+
} catch {
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1253
1450
|
/** The lifetime profile, in milliseconds: pending on each route, approved, and a download's question. */
|
|
1254
1451
|
const APPROVAL_LIFETIMES = Object.freeze({
|
|
1255
1452
|
chat: 6e5,
|
|
@@ -1327,18 +1524,25 @@ function listedEntry(file) {
|
|
|
1327
1524
|
* folders it offered, and the listing exactly as stored, left out when the record has none.
|
|
1328
1525
|
*/
|
|
1329
1526
|
function bindingObjectOf(record) {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1527
|
+
if (record.kind === "disclosure" && "disclosure" in record && record.disclosure !== void 0) return {
|
|
1528
|
+
v: 2,
|
|
1529
|
+
kind: "disclosure",
|
|
1530
|
+
approvalId: record.approvalId,
|
|
1531
|
+
disclosure: canonicalDisclosureBinding(record.disclosure)
|
|
1532
|
+
};
|
|
1533
|
+
const ordinary = record;
|
|
1534
|
+
const identity = identityOf(ordinary);
|
|
1535
|
+
if (ordinary.kind === "download") {
|
|
1536
|
+
const download = ordinary.download;
|
|
1333
1537
|
if (download === void 0) throw new CommsError("BAD_DATA", "a download’s approval does not hold the question it was asked for");
|
|
1334
1538
|
return {
|
|
1335
1539
|
v: 2,
|
|
1336
1540
|
kind: "download",
|
|
1337
|
-
contentDigest:
|
|
1541
|
+
contentDigest: ordinary.contentDigest,
|
|
1338
1542
|
profile: {
|
|
1339
|
-
pendingMs:
|
|
1340
|
-
policy:
|
|
1341
|
-
requiredPolicy:
|
|
1543
|
+
pendingMs: ordinary.pendingMs,
|
|
1544
|
+
policy: ordinary.policy,
|
|
1545
|
+
requiredPolicy: ordinary.requiredPolicy
|
|
1342
1546
|
},
|
|
1343
1547
|
identity,
|
|
1344
1548
|
offered: {
|
|
@@ -1350,11 +1554,11 @@ function bindingObjectOf(record) {
|
|
|
1350
1554
|
}
|
|
1351
1555
|
return {
|
|
1352
1556
|
v: 2,
|
|
1353
|
-
kind:
|
|
1354
|
-
contentDigest:
|
|
1355
|
-
route:
|
|
1356
|
-
pendingMs:
|
|
1357
|
-
approvedMs:
|
|
1557
|
+
kind: ordinary.kind,
|
|
1558
|
+
contentDigest: ordinary.contentDigest,
|
|
1559
|
+
route: ordinary.route,
|
|
1560
|
+
pendingMs: ordinary.pendingMs,
|
|
1561
|
+
approvedMs: ordinary.approvedMs,
|
|
1358
1562
|
identity
|
|
1359
1563
|
};
|
|
1360
1564
|
}
|
|
@@ -6342,7 +6546,11 @@ const NO_CONFIGURATION = "this approval store was opened without a configuration
|
|
|
6342
6546
|
const sentAtWords = (usedAt) => `sent at ${usedAt}`;
|
|
6343
6547
|
/** The refusal prefix and detail key of each kind: a send sends nothing, a change changes nothing, a question saves nothing. */
|
|
6344
6548
|
function refusalOf(kind) {
|
|
6345
|
-
const [prefix, idKey, hint] = kind === "
|
|
6549
|
+
const [prefix, idKey, hint] = kind === "disclosure" ? [
|
|
6550
|
+
"nothing was disclosed",
|
|
6551
|
+
"approvalId",
|
|
6552
|
+
"Prepare a new standing disclosure authorisation and show its exact activation preview to the person."
|
|
6553
|
+
] : kind === "change" ? [
|
|
6346
6554
|
"nothing was changed",
|
|
6347
6555
|
"approvalId",
|
|
6348
6556
|
"Prepare the change again and show the new preview to the user."
|
|
@@ -6369,7 +6577,7 @@ function refusalOf(kind) {
|
|
|
6369
6577
|
* or one a pinned surface may not see — byte for byte the same, with `approval: null`, and nothing of any record in it.
|
|
6370
6578
|
*/
|
|
6371
6579
|
function approvalNotFound(approvalId, kind) {
|
|
6372
|
-
return new CommsError("NOT_FOUND", `${kind === "change" ? "nothing was changed" : kind === "download" ? "nothing was saved" : "nothing was sent"}: no approval ${approvalId}`, {
|
|
6580
|
+
return new CommsError("NOT_FOUND", `${kind === "disclosure" ? "nothing was disclosed" : kind === "change" ? "nothing was changed" : kind === "download" ? "nothing was saved" : "nothing was sent"}: no approval ${approvalId}`, {
|
|
6373
6581
|
hint: "Check the id: an approval is only found by the surface, the kind and the owner it was prepared for.",
|
|
6374
6582
|
details: { approval: null }
|
|
6375
6583
|
});
|
|
@@ -6401,6 +6609,24 @@ function objectOf(record, state, claimable, ownerRemoved, reason) {
|
|
|
6401
6609
|
...optional("expiredAt", record.expiredAt)
|
|
6402
6610
|
};
|
|
6403
6611
|
}
|
|
6612
|
+
/** Standing disclosure has no channel owner, draft, policy or sender-controlled field to classify. */
|
|
6613
|
+
function disclosureObjectOf(record, claimable, reason) {
|
|
6614
|
+
return {
|
|
6615
|
+
id: record.approvalId,
|
|
6616
|
+
kind: "disclosure",
|
|
6617
|
+
channel: null,
|
|
6618
|
+
state: record.state,
|
|
6619
|
+
claimable,
|
|
6620
|
+
...reason === void 0 ? {} : { reason },
|
|
6621
|
+
createdAt: record.createdAt,
|
|
6622
|
+
expiresAt: record.expiresAt,
|
|
6623
|
+
...record.approvedAt === void 0 ? {} : { approvedAt: record.approvedAt },
|
|
6624
|
+
...record.usableUntil === void 0 ? {} : { usableUntil: record.usableUntil },
|
|
6625
|
+
...record.usedAt === void 0 ? {} : { usedAt: record.usedAt },
|
|
6626
|
+
...record.revokedAt === void 0 ? {} : { revokedAt: record.revokedAt },
|
|
6627
|
+
...record.expiredAt === void 0 ? {} : { expiredAt: record.expiredAt }
|
|
6628
|
+
};
|
|
6629
|
+
}
|
|
6404
6630
|
Object.freeze([
|
|
6405
6631
|
"expect.to",
|
|
6406
6632
|
"expect.cc",
|
|
@@ -6436,7 +6662,7 @@ function saidOf(approval, facts, options) {
|
|
|
6436
6662
|
if (approval.state === "answered") return facts.answeredWords;
|
|
6437
6663
|
if (approval.state === "expired") {
|
|
6438
6664
|
if (facts.kind === "download") return downloadExpiredWords(facts.answered);
|
|
6439
|
-
const nothing =
|
|
6665
|
+
const nothing = facts.kind === "change" ? "nothing was changed with it" : facts.kind === "disclosure" ? "nothing was disclosed with it" : "nothing was sent with it";
|
|
6440
6666
|
return approval.reason === "clock-anomaly" ? `the clock moved backwards; this approval was expired safely at ${approval.expiredAt}; ${nothing}` : `this approval expired; ${nothing}`;
|
|
6441
6667
|
}
|
|
6442
6668
|
if (approval.state === "used" && facts.kind === "send" && approval.usedAt !== void 0) return options.usedSaid?.(approval.usedAt) ?? `accepted by ${approval.channel === null ? "its provider" : labelOf$1(approval.channel)} at ${approval.usedAt}`;
|
|
@@ -6456,7 +6682,7 @@ function publicApproval(stored, outcome, options = {}) {
|
|
|
6456
6682
|
field,
|
|
6457
6683
|
id: approval.id
|
|
6458
6684
|
}, boundary));
|
|
6459
|
-
const source = stored.form === "v2" ? {
|
|
6685
|
+
const source = stored.form === "v2" && stored.record.kind !== "disclosure" ? {
|
|
6460
6686
|
inboxId: stored.record.inboxId,
|
|
6461
6687
|
draftId: stored.record.draftId,
|
|
6462
6688
|
expect: stored.record.expect,
|
|
@@ -6470,7 +6696,7 @@ function publicApproval(stored, outcome, options = {}) {
|
|
|
6470
6696
|
download: stored.record.download,
|
|
6471
6697
|
answered: stored.record.approvedVia !== void 0,
|
|
6472
6698
|
answeredWords: void 0
|
|
6473
|
-
} : stored.form === "corrupt" && stored.safe !== null ? {
|
|
6699
|
+
} : stored.form === "corrupt" && stored.safe !== null && stored.safe.kind !== "disclosure" ? {
|
|
6474
6700
|
inboxId: stored.safe.inboxId,
|
|
6475
6701
|
draftId: stored.safe.draftId,
|
|
6476
6702
|
expect: stored.safe.expect,
|
|
@@ -6511,6 +6737,7 @@ function publicApproval(stored, outcome, options = {}) {
|
|
|
6511
6737
|
* nothing claimable. Pure: what decides is the store's locked classification, and this only says where it stands.
|
|
6512
6738
|
*/
|
|
6513
6739
|
function approvalObjectOf(record, live, now) {
|
|
6740
|
+
if (record.kind === "disclosure") return classifyDisclosure(record).approval;
|
|
6514
6741
|
return classifyV2(record, {
|
|
6515
6742
|
action: "inspect",
|
|
6516
6743
|
live,
|
|
@@ -6590,7 +6817,7 @@ function approvalOutcome(stored, context) {
|
|
|
6590
6817
|
approval: {
|
|
6591
6818
|
id,
|
|
6592
6819
|
kind: safe?.kind ?? null,
|
|
6593
|
-
channel: safe?.channel ?? null,
|
|
6820
|
+
channel: safe?.kind === "disclosure" ? null : safe?.channel ?? null,
|
|
6594
6821
|
state: "corrupt",
|
|
6595
6822
|
claimable: false,
|
|
6596
6823
|
reason
|
|
@@ -6601,6 +6828,7 @@ function approvalOutcome(stored, context) {
|
|
|
6601
6828
|
return classifyV2(stored.record, context);
|
|
6602
6829
|
}
|
|
6603
6830
|
function classifyV2(read, context) {
|
|
6831
|
+
if (read.kind === "disclosure") return classifyDisclosure(read);
|
|
6604
6832
|
const { live, action } = context;
|
|
6605
6833
|
const at = context.now.toISOString();
|
|
6606
6834
|
const ownerRemoved = read.ownerScope === "owner" && live?.owner === "removed";
|
|
@@ -6649,12 +6877,42 @@ function classifyV2(read, context) {
|
|
|
6649
6877
|
...error === void 0 ? {} : { error }
|
|
6650
6878
|
};
|
|
6651
6879
|
}
|
|
6880
|
+
/** The isolated disclosure state machine never follows a config policy or a channel-owner fence. */
|
|
6881
|
+
function classifyDisclosure(record) {
|
|
6882
|
+
const claimable = record.state === "approved";
|
|
6883
|
+
const approval = disclosureObjectOf(record, claimable, record.reason);
|
|
6884
|
+
let error;
|
|
6885
|
+
switch (record.state) {
|
|
6886
|
+
case "pending":
|
|
6887
|
+
case "approved": break;
|
|
6888
|
+
case "used":
|
|
6889
|
+
error = refusalOf("disclosure")("APPROVAL_VOID", `the standing disclosure approval was used already at ${record.usedAt}`, record, approval);
|
|
6890
|
+
break;
|
|
6891
|
+
case "expired":
|
|
6892
|
+
error = refusalOf("disclosure")("APPROVAL_EXPIRED", "the standing disclosure approval expired unused", record, approval);
|
|
6893
|
+
break;
|
|
6894
|
+
case "revoked":
|
|
6895
|
+
error = refusalOf("disclosure")("APPROVAL_VOID", `the standing disclosure approval was voided (${record.reason ?? "revoked"})`, record, approval);
|
|
6896
|
+
break;
|
|
6897
|
+
default: error = refusalOf("disclosure")("APPROVAL_VOID", "the standing disclosure approval is in an invalid state", record, approval);
|
|
6898
|
+
}
|
|
6899
|
+
return {
|
|
6900
|
+
state: record.state,
|
|
6901
|
+
claimable,
|
|
6902
|
+
...record.reason === void 0 ? {} : { reason: record.reason },
|
|
6903
|
+
record,
|
|
6904
|
+
revokes: false,
|
|
6905
|
+
approval,
|
|
6906
|
+
...error === void 0 ? {} : { error }
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6652
6909
|
/** Whether an active record can be claimed under `live` (D2, `claimable`). */
|
|
6653
6910
|
function isClaimable(record, live) {
|
|
6654
6911
|
switch (record.kind) {
|
|
6655
6912
|
case "send": return record.state === "approved" || record.route === "chat" && live.sendPolicy === "chat";
|
|
6656
6913
|
case "change": return record.state === "approved" || record.route === "chat" && live.changePolicy === "chat";
|
|
6657
6914
|
case "download": return downloadClaimable(record, live.changePolicy);
|
|
6915
|
+
case "disclosure": return record.state === "approved";
|
|
6658
6916
|
}
|
|
6659
6917
|
}
|
|
6660
6918
|
/**
|
|
@@ -6712,6 +6970,7 @@ function errorOf(record, approval, context) {
|
|
|
6712
6970
|
});
|
|
6713
6971
|
}
|
|
6714
6972
|
case "used":
|
|
6973
|
+
if (record.kind === "disclosure") return refuse("APPROVAL_VOID", `the standing disclosure approval was used already at ${record.usedAt}`, record, approval);
|
|
6715
6974
|
if (download) return refuse("APPROVAL_VOID", "the question was answered already, and an answer is used once", record, approval);
|
|
6716
6975
|
if (record.kind === "change") return refuse("APPROVAL_VOID", `the approved change was already claimed at ${record.usedAt}`, record, approval);
|
|
6717
6976
|
return refuse("APPROVAL_VOID", `the approval was used already: it was ${context.usedSaid(String(record.usedAt))}, message id ${record.sentMessageId}`, record, approval, "Prepare a new send only for a new message.");
|
|
@@ -6922,10 +7181,24 @@ function refuseDownload(code, reason, record, hint) {
|
|
|
6922
7181
|
} : {}
|
|
6923
7182
|
});
|
|
6924
7183
|
}
|
|
7184
|
+
/** The disclosure path has no send, draft or provider outcome: its refusal must never imply one. */
|
|
7185
|
+
function refuseDisclosure(code, reason, record, hint) {
|
|
7186
|
+
return new CommsError(code, `nothing was disclosed: ${reason}`, {
|
|
7187
|
+
hint: hint ?? "Prepare a new standing disclosure authorisation and show its exact activation preview to the person.",
|
|
7188
|
+
details: record ? {
|
|
7189
|
+
approvalId: record.approvalId,
|
|
7190
|
+
state: record.state
|
|
7191
|
+
} : {}
|
|
7192
|
+
});
|
|
7193
|
+
}
|
|
6925
7194
|
/** The refusal in the words of the record's own kind. */
|
|
6926
7195
|
function refusalFor(record) {
|
|
6927
|
-
|
|
6928
|
-
|
|
7196
|
+
switch (approvalKind(record)) {
|
|
7197
|
+
case "send": return refuse$1;
|
|
7198
|
+
case "change": return refuseChange;
|
|
7199
|
+
case "download": return refuseDownload;
|
|
7200
|
+
case "disclosure": return refuseDisclosure;
|
|
7201
|
+
}
|
|
6929
7202
|
}
|
|
6930
7203
|
/**
|
|
6931
7204
|
* The refusal of a record prepared under another digest version, in the words of its own kind: the one every locked
|
|
@@ -6942,23 +7215,40 @@ function otherVersionRefusal(record) {
|
|
|
6942
7215
|
* nothing is sent by answering one. The record is as it was, and the hint says so.
|
|
6943
7216
|
*/
|
|
6944
7217
|
function cancelledClaim(record) {
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
7218
|
+
switch (approvalKind(record)) {
|
|
7219
|
+
case "send": return new CommsError("USAGE", "cancelled: nothing was sent", {
|
|
7220
|
+
hint: "The approval was not used: the same call, made again, can still use it until it expires.",
|
|
7221
|
+
details: {
|
|
7222
|
+
approvalId: record.approvalId,
|
|
7223
|
+
state: record.state,
|
|
7224
|
+
reason: "cancelled"
|
|
7225
|
+
}
|
|
7226
|
+
});
|
|
7227
|
+
case "change": return new CommsError("USAGE", "cancelled: nothing was changed", {
|
|
7228
|
+
hint: "The approval was not used: the same call, made again, can still use it until it expires.",
|
|
7229
|
+
details: {
|
|
7230
|
+
approvalId: record.approvalId,
|
|
7231
|
+
state: record.state,
|
|
7232
|
+
reason: "cancelled"
|
|
7233
|
+
}
|
|
7234
|
+
});
|
|
7235
|
+
case "download": return new CommsError("USAGE", "cancelled: nothing was saved", {
|
|
7236
|
+
hint: "The question was not used: the same call, made again with the same answer, can still use it until it expires.",
|
|
7237
|
+
details: {
|
|
7238
|
+
choiceId: record.approvalId,
|
|
7239
|
+
state: record.state,
|
|
7240
|
+
reason: "cancelled"
|
|
7241
|
+
}
|
|
7242
|
+
});
|
|
7243
|
+
case "disclosure": return new CommsError("USAGE", "cancelled: nothing was disclosed", {
|
|
7244
|
+
hint: "The standing disclosure approval was not used: it can still be completed until it expires.",
|
|
7245
|
+
details: {
|
|
7246
|
+
approvalId: record.approvalId,
|
|
7247
|
+
state: record.state,
|
|
7248
|
+
reason: "cancelled"
|
|
7249
|
+
}
|
|
7250
|
+
});
|
|
7251
|
+
}
|
|
6962
7252
|
}
|
|
6963
7253
|
/** The refusal of a renewal or an outcome from a call that does not hold the send's claim. It names no token. */
|
|
6964
7254
|
function notTheClaim(approvalId) {
|
|
@@ -6985,7 +7275,9 @@ function approvePendingHint(handoffs, approvalId, platform) {
|
|
|
6985
7275
|
function matchesExpectation(found, expect) {
|
|
6986
7276
|
const kind = kindOf(found);
|
|
6987
7277
|
const owner = ownerOf(found);
|
|
6988
|
-
if (kind === null
|
|
7278
|
+
if (kind === null) return expect.owner === void 0;
|
|
7279
|
+
if (kind === "disclosure") return (expect.kind === void 0 || expect.kind === "disclosure") && expect.channel === void 0 && expect.owner === void 0;
|
|
7280
|
+
if (owner === null) return expect.owner === void 0;
|
|
6989
7281
|
if (expect.kind !== void 0 && kind !== expect.kind) return false;
|
|
6990
7282
|
if (expect.channel !== void 0 && channelOf(found) !== expect.channel) return false;
|
|
6991
7283
|
return expect.owner === void 0 || owner === expect.owner;
|
|
@@ -7110,6 +7402,18 @@ var ApprovalStore = class {
|
|
|
7110
7402
|
* profile checked first, so nothing is written that names a channel this release does not know.
|
|
7111
7403
|
*/
|
|
7112
7404
|
#bound(record) {
|
|
7405
|
+
if (record.kind === "disclosure") {
|
|
7406
|
+
const disclosure = canonicalDisclosureBinding(record.disclosure);
|
|
7407
|
+
return {
|
|
7408
|
+
...record,
|
|
7409
|
+
disclosure,
|
|
7410
|
+
bindingDigest: bindingDigestOf({
|
|
7411
|
+
approvalId: record.approvalId,
|
|
7412
|
+
kind: "disclosure",
|
|
7413
|
+
disclosure
|
|
7414
|
+
})
|
|
7415
|
+
};
|
|
7416
|
+
}
|
|
7113
7417
|
requireKnownChannel(record.channel);
|
|
7114
7418
|
return {
|
|
7115
7419
|
...record,
|
|
@@ -7157,6 +7461,28 @@ var ApprovalStore = class {
|
|
|
7157
7461
|
await this.#write(record);
|
|
7158
7462
|
return record;
|
|
7159
7463
|
}
|
|
7464
|
+
/** Creates only a pending standing disclosure approval from a daemon-derived canonical binding. */
|
|
7465
|
+
async createDisclosure(binding) {
|
|
7466
|
+
await this.ensurePruned();
|
|
7467
|
+
const disclosure = canonicalDisclosureBinding(binding);
|
|
7468
|
+
const now = this.#now();
|
|
7469
|
+
const pendingMs = APPROVAL_LIFETIMES.confirm;
|
|
7470
|
+
const record = this.#bound({
|
|
7471
|
+
approvalId: newApprovalId(),
|
|
7472
|
+
kind: "disclosure",
|
|
7473
|
+
digestVersion: 2,
|
|
7474
|
+
pendingMs,
|
|
7475
|
+
approvedMs: APPROVAL_LIFETIMES.approved,
|
|
7476
|
+
disclosure,
|
|
7477
|
+
challengeAttempts: 0,
|
|
7478
|
+
state: "pending",
|
|
7479
|
+
createdAt: now.toISOString(),
|
|
7480
|
+
expiresAt: new Date(now.getTime() + pendingMs).toISOString(),
|
|
7481
|
+
updatedAt: now.toISOString()
|
|
7482
|
+
});
|
|
7483
|
+
await this.#write(record);
|
|
7484
|
+
return record;
|
|
7485
|
+
}
|
|
7160
7486
|
/**
|
|
7161
7487
|
* One approval, in whichever of its four forms it is (`StoredApproval`), read against one configuration snapshot —
|
|
7162
7488
|
* or null when there is none. Nothing is written.
|
|
@@ -7307,17 +7633,33 @@ var ApprovalStore = class {
|
|
|
7307
7633
|
* approval can be used as it is.
|
|
7308
7634
|
*/
|
|
7309
7635
|
#alreadyApproved(record, outcome) {
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7636
|
+
const detail = {
|
|
7637
|
+
approvalId: record.approvalId,
|
|
7638
|
+
state: record.state,
|
|
7639
|
+
approval: outcome.approval
|
|
7640
|
+
};
|
|
7641
|
+
switch (record.kind) {
|
|
7642
|
+
case "send": return new CommsError("USAGE", "nothing was sent: it is approved already, and a person approves it once", {
|
|
7643
|
+
hint: "Nothing more is needed from the person: try the same call again with the same approval.",
|
|
7644
|
+
details: detail
|
|
7645
|
+
});
|
|
7646
|
+
case "change": return new CommsError("USAGE", "nothing was changed: it is approved already, and a person approves it once", {
|
|
7647
|
+
hint: "Nothing more is needed from the person: try the same call again with the same approval.",
|
|
7648
|
+
details: detail
|
|
7649
|
+
});
|
|
7650
|
+
case "download": return new CommsError("USAGE", "nothing was saved: it is approved already, and a person approves it once", {
|
|
7651
|
+
hint: "Nothing more is needed from the person: try the same call again with the same approval.",
|
|
7652
|
+
details: detail
|
|
7653
|
+
});
|
|
7654
|
+
case "disclosure": return new CommsError("USAGE", "nothing was disclosed: it is approved already, and a person approves it once", {
|
|
7655
|
+
hint: "Nothing more is needed from the person: complete the same activation with the same approval.",
|
|
7656
|
+
details: detail
|
|
7657
|
+
});
|
|
7658
|
+
}
|
|
7318
7659
|
}
|
|
7319
7660
|
/** Issues a new challenge to show a human; only its hash is kept. */
|
|
7320
7661
|
async issueChallenge(approvalId, kind = "send", _platform = process.platform, options = {}) {
|
|
7662
|
+
if (kind === "disclosure") throw new CommsError("APPROVAL_REQUIRED", "nothing was disclosed: a standing disclosure challenge is only issued to the trusted terminal or app route");
|
|
7321
7663
|
const challenge = newChallenge();
|
|
7322
7664
|
await this.#transition(approvalId, { kind }, "approve", (current, outcome) => {
|
|
7323
7665
|
if (outcome.error) throw outcome.error;
|
|
@@ -7329,6 +7671,139 @@ var ApprovalStore = class {
|
|
|
7329
7671
|
}, { usedSaid: options.usedSaid });
|
|
7330
7672
|
return challenge;
|
|
7331
7673
|
}
|
|
7674
|
+
/** Issues the typed challenge that a terminal or the trusted desktop app must answer for disclosure. */
|
|
7675
|
+
async issueDisclosureChallenge(approvalId) {
|
|
7676
|
+
const challenge = newChallenge();
|
|
7677
|
+
await this.#transition(approvalId, { kind: "disclosure" }, "approve", (current, outcome) => {
|
|
7678
|
+
if (outcome.error) throw outcome.error;
|
|
7679
|
+
if (current.state !== "pending") throw this.#alreadyApproved(current, outcome);
|
|
7680
|
+
return {
|
|
7681
|
+
...current,
|
|
7682
|
+
challengeHash: hashChallenge(challenge)
|
|
7683
|
+
};
|
|
7684
|
+
});
|
|
7685
|
+
return challenge;
|
|
7686
|
+
}
|
|
7687
|
+
/**
|
|
7688
|
+
* Approves one exact standing disclosure authority. MCP/chat forms are never an approval channel here: `app` means
|
|
7689
|
+
* the desktop application's trusted typed-challenge bridge, not a client form.
|
|
7690
|
+
*/
|
|
7691
|
+
async approveDisclosure(approvalId, liveBinding, answer, via) {
|
|
7692
|
+
let failure = null;
|
|
7693
|
+
const done = await this.#transition(approvalId, { kind: "disclosure" }, "approve", (current, outcome) => {
|
|
7694
|
+
if (outcome.error) throw outcome.error;
|
|
7695
|
+
if (current.state !== "pending") throw this.#alreadyApproved(current, outcome);
|
|
7696
|
+
if (via !== "terminal" && via !== "app") throw new CommsError("APPROVAL_REQUIRED", "nothing was disclosed: approval is only at the terminal or the trusted app", { details: {
|
|
7697
|
+
approvalId: current.approvalId,
|
|
7698
|
+
state: current.state
|
|
7699
|
+
} });
|
|
7700
|
+
if (!current.challengeHash) throw new CommsError("APPROVAL_REQUIRED", "nothing was disclosed: no challenge was issued for this approval", { details: {
|
|
7701
|
+
approvalId: current.approvalId,
|
|
7702
|
+
state: current.state
|
|
7703
|
+
} });
|
|
7704
|
+
const at = this.#now();
|
|
7705
|
+
const binding = current.disclosure;
|
|
7706
|
+
if (!sameDisclosureBinding(binding, liveBinding)) {
|
|
7707
|
+
failure = {
|
|
7708
|
+
code: "APPROVAL_VOID",
|
|
7709
|
+
reason: "the disclosure binding changed after it was prepared"
|
|
7710
|
+
};
|
|
7711
|
+
return {
|
|
7712
|
+
...current,
|
|
7713
|
+
state: "revoked",
|
|
7714
|
+
reason: failure.reason,
|
|
7715
|
+
revokedAt: at.toISOString(),
|
|
7716
|
+
challengeHash: void 0
|
|
7717
|
+
};
|
|
7718
|
+
}
|
|
7719
|
+
if (!challengeMatches(answer, current.challengeHash)) {
|
|
7720
|
+
const attempts = current.challengeAttempts + 1;
|
|
7721
|
+
if (attempts >= 3) {
|
|
7722
|
+
failure = {
|
|
7723
|
+
code: "APPROVAL_VOID",
|
|
7724
|
+
reason: "too many wrong answers to the challenge"
|
|
7725
|
+
};
|
|
7726
|
+
return {
|
|
7727
|
+
...current,
|
|
7728
|
+
challengeAttempts: attempts,
|
|
7729
|
+
state: "revoked",
|
|
7730
|
+
reason: failure.reason,
|
|
7731
|
+
revokedAt: at.toISOString()
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
failure = {
|
|
7735
|
+
code: "APPROVAL_REQUIRED",
|
|
7736
|
+
reason: "the challenge did not match"
|
|
7737
|
+
};
|
|
7738
|
+
return {
|
|
7739
|
+
...current,
|
|
7740
|
+
challengeAttempts: attempts
|
|
7741
|
+
};
|
|
7742
|
+
}
|
|
7743
|
+
return {
|
|
7744
|
+
...current,
|
|
7745
|
+
state: "approved",
|
|
7746
|
+
approvedVia: via,
|
|
7747
|
+
approvedAt: at.toISOString(),
|
|
7748
|
+
usableUntil: new Date(at.getTime() + APPROVAL_LIFETIMES.approved).toISOString(),
|
|
7749
|
+
approvedDigest: binding.digest,
|
|
7750
|
+
challengeHash: void 0
|
|
7751
|
+
};
|
|
7752
|
+
});
|
|
7753
|
+
const refused = failure;
|
|
7754
|
+
if (refused !== null) throw refusedAfter(refusalFor(done.record)(refused.code, refused.reason, done.record), done);
|
|
7755
|
+
return done.record;
|
|
7756
|
+
}
|
|
7757
|
+
/**
|
|
7758
|
+
* Claims an approved disclosure once, persisting the exact `usedAt` that recovery must subsequently reuse.
|
|
7759
|
+
* A specialised caller may impose a shorter deadline; it is evaluated while this record's transition lock is held,
|
|
7760
|
+
* so a check made before waiting on that lock cannot accidentally consume the approval after it has expired.
|
|
7761
|
+
*/
|
|
7762
|
+
async claimForDisclosure(approvalId, liveBinding, options = {}) {
|
|
7763
|
+
if (options.deadlineMs !== void 0 && (!Number.isSafeInteger(options.deadlineMs) || options.deadlineMs <= 0)) throw new CommsError("BAD_DATA", "the disclosure claim deadline is not a readable instant");
|
|
7764
|
+
let failure = null;
|
|
7765
|
+
const done = await this.#transition(approvalId, { kind: "disclosure" }, "claim", (current, outcome) => {
|
|
7766
|
+
if (outcome.error) throw outcome.error;
|
|
7767
|
+
const now = this.#now();
|
|
7768
|
+
if (options.deadlineMs !== void 0 && now.getTime() >= options.deadlineMs) throw new CommsError("APPROVAL_VOID", "nothing was disclosed: the disclosure challenge expired before claim", { details: {
|
|
7769
|
+
approvalId: current.approvalId,
|
|
7770
|
+
state: current.state,
|
|
7771
|
+
reason: "CHALLENGE_EXPIRED"
|
|
7772
|
+
} });
|
|
7773
|
+
const at = now.toISOString();
|
|
7774
|
+
const binding = current.disclosure;
|
|
7775
|
+
if (current.state === "pending") throw new CommsError("APPROVAL_PENDING", "nothing was disclosed: the standing disclosure approval is pending", { details: {
|
|
7776
|
+
approvalId: current.approvalId,
|
|
7777
|
+
state: current.state
|
|
7778
|
+
} });
|
|
7779
|
+
if (current.state !== "approved") throw new CommsError("APPROVAL_VOID", "nothing was disclosed: the standing disclosure approval was used already", { details: {
|
|
7780
|
+
approvalId: current.approvalId,
|
|
7781
|
+
state: current.state
|
|
7782
|
+
} });
|
|
7783
|
+
if (!sameDisclosureBinding(binding, liveBinding) || current.approvedDigest !== binding.digest) {
|
|
7784
|
+
failure = {
|
|
7785
|
+
code: "APPROVAL_VOID",
|
|
7786
|
+
reason: "the disclosure binding changed after it was approved"
|
|
7787
|
+
};
|
|
7788
|
+
return {
|
|
7789
|
+
...current,
|
|
7790
|
+
state: "revoked",
|
|
7791
|
+
reason: failure.reason,
|
|
7792
|
+
revokedAt: at,
|
|
7793
|
+
challengeHash: void 0
|
|
7794
|
+
};
|
|
7795
|
+
}
|
|
7796
|
+
return {
|
|
7797
|
+
...current,
|
|
7798
|
+
state: "used",
|
|
7799
|
+
usedAt: at
|
|
7800
|
+
};
|
|
7801
|
+
});
|
|
7802
|
+
const refused = failure;
|
|
7803
|
+
if (refused !== null) throw refusedAfter(refusalFor(done.record)(refused.code, refused.reason, done.record), done);
|
|
7804
|
+
await this.#markClaimed(done);
|
|
7805
|
+
return done.record;
|
|
7806
|
+
}
|
|
7332
7807
|
/**
|
|
7333
7808
|
* A human approved through a confirm channel by typing the issued challenge. The draft must still be exactly what the
|
|
7334
7809
|
* record was prepared for; otherwise the record is voided, because the human would be approving content the record
|
|
@@ -7338,6 +7813,7 @@ var ApprovalStore = class {
|
|
|
7338
7813
|
* `createChange`), so a person's typed code means one thing whichever kind of approval it is typed for.
|
|
7339
7814
|
*/
|
|
7340
7815
|
async approve(approvalId, via, live, answer, kind = "send", _platform = process.platform, options = {}) {
|
|
7816
|
+
if (kind === "disclosure") throw new CommsError("APPROVAL_REQUIRED", "nothing was disclosed: a standing disclosure approval is only accepted on the trusted terminal or app route");
|
|
7341
7817
|
let failure = null;
|
|
7342
7818
|
const done = await this.#transition(approvalId, { kind }, "approve", (current, outcome) => {
|
|
7343
7819
|
if (outcome.error) throw outcome.error;
|
|
@@ -8072,6 +8548,7 @@ function fail(reason, attribution = "verified") {
|
|
|
8072
8548
|
function checkVerified(record, raw) {
|
|
8073
8549
|
if (raw.digestVersion !== 2) return fail("digest-version");
|
|
8074
8550
|
const kind = raw.kind;
|
|
8551
|
+
if (kind === "disclosure") return checkDisclosure(record, raw);
|
|
8075
8552
|
if (kind !== "send" && kind !== "change" && kind !== "download") return fail("wrong-shape");
|
|
8076
8553
|
if (!STATES$1.includes(raw.state)) return fail("state-impossible");
|
|
8077
8554
|
const state = raw.state;
|
|
@@ -8095,6 +8572,59 @@ function checkVerified(record, raw) {
|
|
|
8095
8572
|
}
|
|
8096
8573
|
return checkTimestamps(raw, kind, state, times) ?? checkEvidence(record, raw, kind, state);
|
|
8097
8574
|
}
|
|
8575
|
+
/** Validates the closed disclosure union member without borrowing a sender approval's shape. */
|
|
8576
|
+
function checkDisclosure(record, raw) {
|
|
8577
|
+
if (![
|
|
8578
|
+
"pending",
|
|
8579
|
+
"approved",
|
|
8580
|
+
"used",
|
|
8581
|
+
"expired",
|
|
8582
|
+
"revoked"
|
|
8583
|
+
].includes(raw.state)) return fail("state-impossible");
|
|
8584
|
+
const disclosure = record.disclosure;
|
|
8585
|
+
if (disclosure === void 0) return fail("wrong-shape");
|
|
8586
|
+
let binding;
|
|
8587
|
+
try {
|
|
8588
|
+
binding = canonicalDisclosureBinding(disclosure);
|
|
8589
|
+
} catch {
|
|
8590
|
+
return fail("wrong-shape");
|
|
8591
|
+
}
|
|
8592
|
+
if (raw.pendingMs !== APPROVAL_LIFETIMES.confirm || raw.approvedMs !== APPROVAL_LIFETIMES.approved) return fail("lifetime-mismatch");
|
|
8593
|
+
const times = {};
|
|
8594
|
+
for (const field of TIMESTAMPS) {
|
|
8595
|
+
const at = timeOf(raw[field]);
|
|
8596
|
+
if (at === "invalid") return fail("timestamp-invalid");
|
|
8597
|
+
if (at !== null) times[field] = at;
|
|
8598
|
+
}
|
|
8599
|
+
const { createdAt, expiresAt, approvedAt, usableUntil, usedAt, revokedAt, expiredAt } = times;
|
|
8600
|
+
if (createdAt === void 0 || expiresAt === void 0) return fail("timestamp-missing");
|
|
8601
|
+
if (expiresAt - createdAt !== APPROVAL_LIFETIMES.confirm) return fail("lifetime-mismatch");
|
|
8602
|
+
const state = raw.state;
|
|
8603
|
+
const approved = approvedAt !== void 0 || usableUntil !== void 0;
|
|
8604
|
+
if (approvedAt === void 0 !== (usableUntil === void 0)) return fail("timestamp-missing");
|
|
8605
|
+
if (approvedAt !== void 0 && usableUntil !== void 0) {
|
|
8606
|
+
if (!(createdAt <= approvedAt && approvedAt < expiresAt)) return fail("timestamp-misordered");
|
|
8607
|
+
if (usableUntil - approvedAt !== APPROVAL_LIFETIMES.approved) return fail("lifetime-mismatch");
|
|
8608
|
+
}
|
|
8609
|
+
if (state === "approved" && !approved) return fail("timestamp-missing");
|
|
8610
|
+
if (state === "used" && usedAt === void 0) return fail("timestamp-missing");
|
|
8611
|
+
if (state !== "used" && usedAt !== void 0) return fail("timestamp-misplaced");
|
|
8612
|
+
if (state === "revoked" && revokedAt === void 0) return fail("timestamp-missing");
|
|
8613
|
+
if (state !== "revoked" && revokedAt !== void 0) return fail("timestamp-misplaced");
|
|
8614
|
+
if (state === "expired" && expiredAt === void 0) return fail("timestamp-missing");
|
|
8615
|
+
if (state !== "expired" && expiredAt !== void 0) return fail("timestamp-misplaced");
|
|
8616
|
+
const deadline = usableUntil ?? expiresAt;
|
|
8617
|
+
if (usedAt !== void 0 && !(approvedAt !== void 0 && approvedAt <= usedAt && usedAt < deadline)) return fail("timestamp-misordered");
|
|
8618
|
+
if (revokedAt !== void 0 && !(createdAt <= revokedAt && revokedAt < deadline)) return fail("timestamp-misordered");
|
|
8619
|
+
if (expiredAt !== void 0 && expiredAt < (approvedAt ?? createdAt)) return fail("timestamp-misordered");
|
|
8620
|
+
if (raw.challengeAttempts !== 0 && raw.challengeAttempts !== 1 && raw.challengeAttempts !== 2 && raw.challengeAttempts !== 3) return fail("evidence-invalid");
|
|
8621
|
+
const via = raw.approvedVia;
|
|
8622
|
+
const digest = raw.approvedDigest;
|
|
8623
|
+
if (!approved) {
|
|
8624
|
+
if (via !== void 0 || digest !== void 0) return fail("evidence-contradictory");
|
|
8625
|
+
} else if (via !== "terminal" && via !== "app" || digest !== binding.digest) return fail("evidence-contradictory");
|
|
8626
|
+
return null;
|
|
8627
|
+
}
|
|
8098
8628
|
/** The identity the binding covers is also consistent with itself: who it belongs to, and what it is about. */
|
|
8099
8629
|
function checkIdentity(record, raw, kind) {
|
|
8100
8630
|
if (!isKnownChannel(raw.channel)) return fail("identity-contradiction");
|
|
@@ -8237,7 +8767,8 @@ const STATES = [
|
|
|
8237
8767
|
const KINDS = [
|
|
8238
8768
|
"send",
|
|
8239
8769
|
"change",
|
|
8240
|
-
"download"
|
|
8770
|
+
"download",
|
|
8771
|
+
"disclosure"
|
|
8241
8772
|
];
|
|
8242
8773
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8243
8774
|
const isString = (value) => typeof value === "string";
|
|
@@ -8294,6 +8825,59 @@ function commonShape(raw) {
|
|
|
8294
8825
|
}
|
|
8295
8826
|
return null;
|
|
8296
8827
|
}
|
|
8828
|
+
/** The disclosure union member is deliberately closed: it cannot acquire a send-shaped field by optionality. */
|
|
8829
|
+
function disclosureShape(raw) {
|
|
8830
|
+
for (const field of [
|
|
8831
|
+
"approvalId",
|
|
8832
|
+
"state",
|
|
8833
|
+
"createdAt",
|
|
8834
|
+
"expiresAt",
|
|
8835
|
+
"updatedAt",
|
|
8836
|
+
"bindingDigest"
|
|
8837
|
+
]) if (!isString(raw[field])) return "wrong-shape";
|
|
8838
|
+
for (const field of [
|
|
8839
|
+
"pendingMs",
|
|
8840
|
+
"approvedMs",
|
|
8841
|
+
"challengeAttempts"
|
|
8842
|
+
]) if (typeof raw[field] !== "number") return "wrong-shape";
|
|
8843
|
+
for (const field of [
|
|
8844
|
+
"approvedDigest",
|
|
8845
|
+
"approvedVia",
|
|
8846
|
+
"approvedAt",
|
|
8847
|
+
"usableUntil",
|
|
8848
|
+
"challengeHash",
|
|
8849
|
+
"usedAt",
|
|
8850
|
+
"revokedAt",
|
|
8851
|
+
"expiredAt",
|
|
8852
|
+
"reason"
|
|
8853
|
+
]) if (!optional(raw[field], isString)) return "wrong-shape";
|
|
8854
|
+
const disclosure = raw.disclosure;
|
|
8855
|
+
if (!isObject(disclosure) || !isString(disclosure.digest) || !isString(disclosure.activationIntentId) || !isString(disclosure.activationKind) || !Array.isArray(disclosure.versions) || !disclosure.versions.every((entry) => isObject(entry) && isString(entry.kind) && isString(entry.id) && typeof entry.version === "number")) return "wrong-shape";
|
|
8856
|
+
for (const field of [
|
|
8857
|
+
"channel",
|
|
8858
|
+
"ownerScope",
|
|
8859
|
+
"route",
|
|
8860
|
+
"inboxId",
|
|
8861
|
+
"inboxSub",
|
|
8862
|
+
"draftId",
|
|
8863
|
+
"draftMessageId",
|
|
8864
|
+
"expect",
|
|
8865
|
+
"sendEpoch",
|
|
8866
|
+
"policy",
|
|
8867
|
+
"requiredPolicy",
|
|
8868
|
+
"riskFlags",
|
|
8869
|
+
"approvedBindingDigest",
|
|
8870
|
+
"sendingAt",
|
|
8871
|
+
"sendingHeartbeatAt",
|
|
8872
|
+
"sentAt",
|
|
8873
|
+
"sentMessageId",
|
|
8874
|
+
"failedAt",
|
|
8875
|
+
"change",
|
|
8876
|
+
"download",
|
|
8877
|
+
"contentDigest"
|
|
8878
|
+
]) if (Object.hasOwn(raw, field)) return "wrong-shape";
|
|
8879
|
+
return null;
|
|
8880
|
+
}
|
|
8297
8881
|
/** The version-2 fields' types: a number where a number belongs, a string where a string does. */
|
|
8298
8882
|
function v2Shape(raw) {
|
|
8299
8883
|
for (const field of [
|
|
@@ -8336,6 +8920,24 @@ function decodeStored(fileId, text, config, now) {
|
|
|
8336
8920
|
}
|
|
8337
8921
|
if (!isObject(raw)) return unreadable(fileId, "wrong-shape");
|
|
8338
8922
|
if (!isString(raw.approvalId)) return unreadable(fileId, "wrong-shape");
|
|
8923
|
+
if (raw.kind === "disclosure") {
|
|
8924
|
+
if (raw.digestVersion === 1) return unreadable(fileId, "wrong-shape");
|
|
8925
|
+
const shape = disclosureShape(raw);
|
|
8926
|
+
if (shape !== null) return unreadable(fileId, shape);
|
|
8927
|
+
const record = raw;
|
|
8928
|
+
const validation = validateV2(record, fileId);
|
|
8929
|
+
if (validation.ok) return {
|
|
8930
|
+
form: "v2",
|
|
8931
|
+
record
|
|
8932
|
+
};
|
|
8933
|
+
return {
|
|
8934
|
+
form: "corrupt",
|
|
8935
|
+
approvalId: fileId,
|
|
8936
|
+
reason: validation.reason,
|
|
8937
|
+
attribution: validation.attribution,
|
|
8938
|
+
safe: validation.attribution === "verified" ? safeFieldsOf(record) : null
|
|
8939
|
+
};
|
|
8940
|
+
}
|
|
8339
8941
|
const shape = commonShape(raw);
|
|
8340
8942
|
if (shape !== null) return unreadable(fileId, shape);
|
|
8341
8943
|
if (raw.digestVersion === 1) {
|
|
@@ -8367,6 +8969,7 @@ function decodeStored(fileId, text, config, now) {
|
|
|
8367
8969
|
};
|
|
8368
8970
|
}
|
|
8369
8971
|
function safeFieldsOf(record) {
|
|
8972
|
+
if (record.kind === "disclosure") return { kind: "disclosure" };
|
|
8370
8973
|
return {
|
|
8371
8974
|
kind: record.kind,
|
|
8372
8975
|
channel: record.channel,
|
|
@@ -8394,9 +8997,9 @@ function asLegacy(stored) {
|
|
|
8394
8997
|
function ownerOf(stored) {
|
|
8395
8998
|
if (stored === null) return null;
|
|
8396
8999
|
switch (stored.form) {
|
|
8397
|
-
case "v2": return stored.record.inboxId;
|
|
9000
|
+
case "v2": return stored.record.kind === "disclosure" ? null : stored.record.inboxId;
|
|
8398
9001
|
case "legacy": return stored.record.inboxId;
|
|
8399
|
-
case "corrupt": return stored.safe?.inboxId ?? null;
|
|
9002
|
+
case "corrupt": return stored.safe?.kind === "disclosure" ? null : stored.safe?.inboxId ?? null;
|
|
8400
9003
|
case "unreadable": return null;
|
|
8401
9004
|
}
|
|
8402
9005
|
}
|
|
@@ -8418,9 +9021,9 @@ function kindOf(stored) {
|
|
|
8418
9021
|
function channelOf(stored) {
|
|
8419
9022
|
if (stored === null) return null;
|
|
8420
9023
|
switch (stored.form) {
|
|
8421
|
-
case "v2": return stored.record.channel;
|
|
9024
|
+
case "v2": return stored.record.kind === "disclosure" ? null : stored.record.channel;
|
|
8422
9025
|
case "legacy": return stored.view.channel;
|
|
8423
|
-
case "corrupt": return stored.safe?.channel ?? null;
|
|
9026
|
+
case "corrupt": return stored.safe?.kind === "disclosure" ? null : stored.safe?.channel ?? null;
|
|
8424
9027
|
case "unreadable": return null;
|
|
8425
9028
|
}
|
|
8426
9029
|
}
|
|
@@ -8569,7 +9172,7 @@ function verdictOf(stored, now) {
|
|
|
8569
9172
|
const providerId = record.kind === "send" && record.state === "used" && typeof record.sentMessageId === "string" ? record.sentMessageId : "";
|
|
8570
9173
|
return {
|
|
8571
9174
|
keep: false,
|
|
8572
|
-
owner: record.inboxId,
|
|
9175
|
+
owner: record.kind === "disclosure" ? "" : record.inboxId,
|
|
8573
9176
|
row: {
|
|
8574
9177
|
kind: record.kind,
|
|
8575
9178
|
state: record.state,
|
|
@@ -9040,7 +9643,7 @@ function folding(platform) {
|
|
|
9040
9643
|
return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
|
|
9041
9644
|
}
|
|
9042
9645
|
/** The package version, read from package.json at build time. */
|
|
9043
|
-
const VERSION = "0.
|
|
9646
|
+
const VERSION = "0.16.0";
|
|
9044
9647
|
/**
|
|
9045
9648
|
* Versions as semver writes and orders them — what an update compares, and what the daily update check's reader needs
|
|
9046
9649
|
* to decide whether the release it last heard of is newer than the one running.
|
|
@@ -11726,7 +12329,7 @@ function serverPruneChange(core, env, request, own) {
|
|
|
11726
12329
|
};
|
|
11727
12330
|
const { packageName, version } = ownProduct(request.channel, own) ?? {
|
|
11728
12331
|
packageName: channelServer(request.channel).packageName,
|
|
11729
|
-
version: "0.
|
|
12332
|
+
version: "0.16.0"
|
|
11730
12333
|
};
|
|
11731
12334
|
const product = {
|
|
11732
12335
|
packageName,
|
|
@@ -13443,7 +14046,7 @@ function terminalUpdateHooks(core, env, options) {
|
|
|
13443
14046
|
};
|
|
13444
14047
|
}
|
|
13445
14048
|
//#endregion
|
|
13446
|
-
//#region ../core/dist/tool-arguments-
|
|
14049
|
+
//#region ../core/dist/tool-arguments-DA5ZqgoB.mjs
|
|
13447
14050
|
const APPLIED = Symbol.for("agentcomms.strictToolArguments");
|
|
13448
14051
|
const TARGET = "draft-2020-12";
|
|
13449
14052
|
/**
|
|
@@ -23087,6 +23690,19 @@ const narrowingSchema = strictObject({
|
|
|
23087
23690
|
flag: string().regex(/^--[a-z][a-z-]*$/, "a long flag, `--like-this`"),
|
|
23088
23691
|
kind: _enum(["pin", "switch"])
|
|
23089
23692
|
});
|
|
23693
|
+
const eventAccessSchema = discriminatedUnion("kind", [
|
|
23694
|
+
strictObject({
|
|
23695
|
+
kind: literal$1("oauth-user"),
|
|
23696
|
+
requiredScopes: array(line).min(1)
|
|
23697
|
+
}),
|
|
23698
|
+
strictObject({ kind: literal$1("resend-full-access") }),
|
|
23699
|
+
strictObject({ kind: literal$1("local-store") })
|
|
23700
|
+
]);
|
|
23701
|
+
const eventsSchema = strictObject({
|
|
23702
|
+
types: array(line).min(1),
|
|
23703
|
+
minimumIntervalMs: number().int().positive(),
|
|
23704
|
+
access: eventAccessSchema
|
|
23705
|
+
});
|
|
23090
23706
|
/**
|
|
23091
23707
|
* The shapes Gmail and Slack had before there was a manifest, kept because their entries, tools and skills already
|
|
23092
23708
|
* say them — and nobody else's.
|
|
@@ -23169,7 +23785,8 @@ strictObject({
|
|
|
23169
23785
|
prefix: string().regex(/^[a-z][a-z0-9]*-$/, "a word and a hyphen: `gmail-`"),
|
|
23170
23786
|
contract: line
|
|
23171
23787
|
}).optional(),
|
|
23172
|
-
approvalGrouping: _enum(["draft", "draft-revision-digest"]).optional()
|
|
23788
|
+
approvalGrouping: _enum(["draft", "draft-revision-digest"]).optional(),
|
|
23789
|
+
events: eventsSchema.optional()
|
|
23173
23790
|
}).superRefine((manifest, ctx) => {
|
|
23174
23791
|
const issue = (path, message) => ctx.addIssue({
|
|
23175
23792
|
code: "custom",
|
|
@@ -24808,13 +25425,11 @@ var Throttle = class {
|
|
|
24808
25425
|
* Counted per holder, a caller gives up only once one holder has kept the lock for five seconds: the stuck lock
|
|
24809
25426
|
* the timeout is there for.
|
|
24810
25427
|
*
|
|
24811
|
-
*
|
|
24812
|
-
*
|
|
24813
|
-
*
|
|
24814
|
-
*
|
|
24815
|
-
*
|
|
24816
|
-
* hanging. `after` waits the same way, because a stop it gave up on recording is a 429 that every other account
|
|
24817
|
-
* carries on into.
|
|
25428
|
+
* An interactive request takes the lock once to reserve its slot and waits outside it. A background event read
|
|
25429
|
+
* reserves nothing while it waits, so a person can always reserve the next free slot; it returns to the lock no
|
|
25430
|
+
* more often than once per interval. The lock is polled, not queued, so its overall wait limit still ends a caller
|
|
25431
|
+
* that keeps losing hand-overs to later callers. `after` waits the same way, because a stop it gave up on recording
|
|
25432
|
+
* is a 429 that every other account carries on into.
|
|
24818
25433
|
*/
|
|
24819
25434
|
#lock;
|
|
24820
25435
|
/** Takes no account on purpose: see above. Every throttle on a state directory is the same one. */
|
|
@@ -24834,7 +25449,8 @@ var Throttle = class {
|
|
|
24834
25449
|
const parsed = JSON.parse(await readFile(this.path, "utf8"));
|
|
24835
25450
|
return {
|
|
24836
25451
|
next: Number.isFinite(parsed.next) ? Number(parsed.next) : 0,
|
|
24837
|
-
blockedUntil: Number.isFinite(parsed.blockedUntil) ? Number(parsed.blockedUntil) : 0
|
|
25452
|
+
blockedUntil: Number.isFinite(parsed.blockedUntil) ? Number(parsed.blockedUntil) : 0,
|
|
25453
|
+
...Number.isFinite(parsed.lastBackgroundAt) ? { lastBackgroundAt: Number(parsed.lastBackgroundAt) } : {}
|
|
24838
25454
|
};
|
|
24839
25455
|
} catch (error) {
|
|
24840
25456
|
if (error.code === "ENOENT" || error instanceof SyntaxError) return {
|
|
@@ -24874,21 +25490,50 @@ var Throttle = class {
|
|
|
24874
25490
|
* still waiting when that one comes back 429, and the stop Resend asked for covers it as much as anything asked
|
|
24875
25491
|
* later.
|
|
24876
25492
|
*/
|
|
24877
|
-
async before() {
|
|
24878
|
-
|
|
24879
|
-
const
|
|
24880
|
-
|
|
24881
|
-
|
|
24882
|
-
|
|
24883
|
-
|
|
24884
|
-
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
|
|
24889
|
-
|
|
24890
|
-
|
|
24891
|
-
|
|
25493
|
+
async before(priority = "interactive") {
|
|
25494
|
+
if (priority === "interactive") {
|
|
25495
|
+
const wait = await this.#locked(async () => {
|
|
25496
|
+
const state = await this.#read();
|
|
25497
|
+
this.#refuseIfHeld(state);
|
|
25498
|
+
const now = this.#now();
|
|
25499
|
+
const slot = Math.max(now, state.next);
|
|
25500
|
+
await writeFileAtomic(this.path, JSON.stringify({
|
|
25501
|
+
next: slot + this.#interval,
|
|
25502
|
+
blockedUntil: state.blockedUntil,
|
|
25503
|
+
...state.lastBackgroundAt === void 0 ? {} : { lastBackgroundAt: state.lastBackgroundAt }
|
|
25504
|
+
}));
|
|
25505
|
+
return slot - now;
|
|
25506
|
+
});
|
|
25507
|
+
if (wait > 0) {
|
|
25508
|
+
await this.#sleep(wait);
|
|
25509
|
+
this.#refuseIfHeld(await this.#read());
|
|
25510
|
+
}
|
|
25511
|
+
return;
|
|
25512
|
+
}
|
|
25513
|
+
for (;;) {
|
|
25514
|
+
const decision = await this.#locked(async () => {
|
|
25515
|
+
const state = await this.#read();
|
|
25516
|
+
this.#refuseIfHeld(state);
|
|
25517
|
+
const now = this.#now();
|
|
25518
|
+
const backgroundWait = state.lastBackgroundAt === void 0 ? 0 : Math.max(0, state.lastBackgroundAt + 2 * this.#interval - now);
|
|
25519
|
+
if (state.next <= now && backgroundWait === 0) {
|
|
25520
|
+
await writeFileAtomic(this.path, JSON.stringify({
|
|
25521
|
+
next: now + this.#interval,
|
|
25522
|
+
blockedUntil: state.blockedUntil,
|
|
25523
|
+
lastBackgroundAt: now
|
|
25524
|
+
}));
|
|
25525
|
+
return {
|
|
25526
|
+
granted: true,
|
|
25527
|
+
wait: 0
|
|
25528
|
+
};
|
|
25529
|
+
}
|
|
25530
|
+
return {
|
|
25531
|
+
granted: false,
|
|
25532
|
+
wait: Math.max(state.next - now, backgroundWait, this.#interval)
|
|
25533
|
+
};
|
|
25534
|
+
});
|
|
25535
|
+
if (decision.granted) return;
|
|
25536
|
+
await this.#sleep(decision.wait);
|
|
24892
25537
|
}
|
|
24893
25538
|
}
|
|
24894
25539
|
/**
|
|
@@ -25244,7 +25889,7 @@ async function resendRequest(transport, method, path, options = {}) {
|
|
|
25244
25889
|
if (options.body !== void 0) headers["content-type"] = "application/json";
|
|
25245
25890
|
if (options.idempotencyKey !== void 0) headers["idempotency-key"] = options.idempotencyKey;
|
|
25246
25891
|
try {
|
|
25247
|
-
await transport.throttle.before();
|
|
25892
|
+
await transport.throttle.before(options.throttlePriority);
|
|
25248
25893
|
} catch (error) {
|
|
25249
25894
|
if (writing && error instanceof CommsError) throw new CommsError(error.code, error.message, {
|
|
25250
25895
|
...error.hint === void 0 ? {} : { hint: error.hint },
|
|
@@ -28045,4 +28690,4 @@ async function revokeSendApproval(context, approvalId) {
|
|
|
28045
28690
|
});
|
|
28046
28691
|
}
|
|
28047
28692
|
//#endregion
|
|
28048
|
-
export {
|
|
28693
|
+
export { checkForUpdates as $, removeAccountChange as A, updateGateAtTerminal as At, strictToolArguments as B, RESEND_DOWNLOAD_ORIGIN as Bt, inspectKey as C, runUpdateCheckChild as Ct, policyChange as D, terminalUpdateHooks as Dt, policyApprovalRefusal as E, stripInvisible as Et, CHANGE_POLICIES as F, writeResult as Ft, SEND_LOOKUP as G, CommsError as H, VERSION$1 as Ht, MODES as I, PACKAGE_NAME as It, approvalsOf as J, UPDATE_CHECK_CHILD_COMMAND as K, SEND_POLICIES as L, RESEND_CALLER as Lt, showAccount as M, waitCallOptions as Mt, resendRequest as N, waitForApproval as Nt, policyReport as O, toCommsError as Ot, ResendContext as P, wholeNumber as Pt, changeToolResult as Q, checkNewName as R, REFUSED as Rt, addAccountChange as S, runCommand as St, modeOf as T, serverPruneChange as Tt, EXIT_CODES as U, CHANNEL_SERVERS as V, ROUTES as Vt, PATH_OPTIONS as W, approveRefusedHint as X, approveChangeAtTerminal as Y, canPrompt as Z, showSentEmail as _, refuseUnclaimedApproval as _t, revokeSendApproval as a, exemptFromUpdateGate as at, readBody as b, renderPrune as bt, downloadReceived as c, handoffSentence as ct, listReceived as d, kindOf as dt, cliHandoffs as et, listScheduled as f, neutralise as ft, showReceived as g, pathOverridesFromCliOptions as gt, resendId as h, paint as ht, prepareSend as i, escapeForDisplay as it, sendPolicyOf as j, updateToolGate as jt, policyWanted as k, truncateDisplay as kt, getMetrics as l, handoffSentenceToFill as lt, listSuppressions as m, openCore as mt, executeSend as n, commandPathOf as nt, sendStatus as o, gatedChange as ot, listSentEmails as p, newBoundary as pt, agentMarker as q, finishSendApproval as r, describeFenceReport as rt, cancelScheduledChange as s, gatedChangeAtTerminal as st, beginSendApproval as t, colorEnabled as tt, listDomains as u, installExitStatus as ut, addressesOf as v, renderApprovalWait as vt, listAccounts as w, serverInstallChange as wt, runDoctor as x, resolvePaths as xt, attachmentRisks as y, renderInstall as yt, keyPermissionOf as z, RESEND_API_ORIGIN as zt };
|