keri-ts 2.2.3 → 2.2.5

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.
Files changed (247) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/registering.js +19 -0
  3. package/esm/keri/npm/src/acdc/salting.js +4 -0
  4. package/esm/keri/npm/src/app/bootstrap/authenticating.js +34 -12
  5. package/esm/keri/npm/src/app/bootstrap/oobiery.js +365 -32
  6. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  7. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  8. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  9. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  10. package/esm/keri/npm/src/app/delivery/forwarding.js +188 -47
  11. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  12. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  13. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  14. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  15. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  16. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  17. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  18. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  19. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  20. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  21. package/esm/keri/npm/src/app/ipex/group-issuance.js +109 -23
  22. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +36 -22
  23. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  24. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  25. package/esm/keri/npm/src/app/ipex/issuance.js +131 -35
  26. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  27. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  28. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  29. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +16 -3
  30. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  31. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  32. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  33. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  34. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  35. package/esm/keri/npm/src/app/multisig/counseling.js +75 -27
  36. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  37. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  38. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  39. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  40. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  41. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  42. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  43. package/esm/keri/npm/src/app/protocol/protocol-processing.js +62 -27
  44. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  45. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  46. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +190 -40
  47. package/esm/keri/npm/src/app/runtime/agent-runtime.js +46 -73
  48. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  49. package/esm/keri/npm/src/app/runtime/doing.js +25 -0
  50. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  51. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  52. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  53. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  54. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  55. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  56. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  57. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  58. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  59. package/esm/keri/npm/src/core/errors.js +73 -0
  60. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  61. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  62. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  63. package/esm/keri/npm/src/core/mailbox/kram-policy.js +8 -0
  64. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  65. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  66. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  67. package/esm/keri/npm/src/core/routing.js +18 -25
  68. package/esm/keri/npm/src/core/scheming.js +12 -5
  69. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  70. package/esm/keri/npm/src/db/basing.js +4 -13
  71. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  72. package/esm/keri/npm/src/db/core/lmdber.js +25 -6
  73. package/esm/keri/npm/src/db/core/memstore.js +4 -0
  74. package/esm/keri/npm/src/db/core/store-owner.js +4 -0
  75. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  76. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  77. package/esm/keri/npm/src/db/subing.js +4 -0
  78. package/esm/keri/npm/src/runtime/index.js +11 -5
  79. package/esm/keri/npm/src/time/mod.js +5 -0
  80. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  81. package/esm/keri/npm/src/vdr/eventing.js +28 -19
  82. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  83. package/esm/keri/npm/src/version.js +2 -2
  84. package/package.json +1 -1
  85. package/types/_dnt.polyfills.d.ts +9 -9
  86. package/types/_dnt.polyfills.d.ts.map +1 -1
  87. package/types/acdc/registering.d.ts +6 -0
  88. package/types/acdc/registering.d.ts.map +1 -1
  89. package/types/acdc/salting.d.ts.map +1 -1
  90. package/types/app/bootstrap/authenticating.d.ts +12 -2
  91. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  92. package/types/app/bootstrap/oobiery.d.ts +144 -7
  93. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  94. package/types/app/credentials/credential-importing.d.ts +8 -0
  95. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  96. package/types/app/credentials/root-replicator.d.ts +19 -8
  97. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  98. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  99. package/types/app/credentials/tel-publishing.d.ts +4 -2
  100. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  101. package/types/app/delivery/forwarding.d.ts +75 -7
  102. package/types/app/delivery/forwarding.d.ts.map +1 -1
  103. package/types/app/delivery/relaying.d.ts +144 -0
  104. package/types/app/delivery/relaying.d.ts.map +1 -0
  105. package/types/app/delivery/respondant.d.ts +9 -2
  106. package/types/app/delivery/respondant.d.ts.map +1 -1
  107. package/types/app/exchanges/exchanging.d.ts +36 -0
  108. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  109. package/types/app/identity/delegating.d.ts +6 -0
  110. package/types/app/identity/delegating.d.ts.map +1 -1
  111. package/types/app/identity/habbing.d.ts +106 -19
  112. package/types/app/identity/habbing.d.ts.map +1 -1
  113. package/types/app/identity/keeping.d.ts +54 -19
  114. package/types/app/identity/keeping.d.ts.map +1 -1
  115. package/types/app/identity/retire-identifier.d.ts +24 -0
  116. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  117. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  118. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  119. package/types/app/ipex/group-ipex.d.ts +60 -14
  120. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  121. package/types/app/ipex/group-issuance.d.ts +48 -6
  122. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  123. package/types/app/ipex/ipex-discloser.d.ts +14 -2
  124. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  125. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  126. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  127. package/types/app/ipex/ipex-v2.d.ts +5 -2
  128. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  129. package/types/app/ipex/issuance.d.ts +35 -11
  130. package/types/app/ipex/issuance.d.ts.map +1 -1
  131. package/types/app/ipex/pending-presentations.d.ts +4 -2
  132. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  133. package/types/app/ipex/presenting.d.ts.map +1 -1
  134. package/types/app/ipex/receive-issuance.d.ts +2 -1
  135. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  136. package/types/app/ipex/unblinding-workflow.d.ts +4 -1
  137. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  138. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  139. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  140. package/types/app/mailbox/forward-handler.d.ts +114 -14
  141. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  142. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  143. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  144. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  145. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  146. package/types/app/mailbox/mailboxing.d.ts +19 -5
  147. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  148. package/types/app/multisig/counseling.d.ts +34 -11
  149. package/types/app/multisig/counseling.d.ts.map +1 -1
  150. package/types/app/multisig/group-members.d.ts +17 -1
  151. package/types/app/multisig/group-members.d.ts.map +1 -1
  152. package/types/app/multisig/grouping.d.ts +17 -1
  153. package/types/app/multisig/grouping.d.ts.map +1 -1
  154. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  155. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  156. package/types/app/multisig/proposing.d.ts +4 -19
  157. package/types/app/multisig/proposing.d.ts.map +1 -1
  158. package/types/app/multisig/rejoin-group.d.ts +119 -0
  159. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  160. package/types/app/multisig/replace-member.d.ts +15 -1
  161. package/types/app/multisig/replace-member.d.ts.map +1 -1
  162. package/types/app/protocol/parsering.d.ts +21 -2
  163. package/types/app/protocol/parsering.d.ts.map +1 -1
  164. package/types/app/protocol/protocol-processing.d.ts +31 -6
  165. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  166. package/types/app/queries/query-replies.d.ts +4 -8
  167. package/types/app/queries/query-replies.d.ts.map +1 -1
  168. package/types/app/queries/relay-refresh.d.ts +26 -0
  169. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  170. package/types/app/queries/witness-inquisitor.d.ts +57 -6
  171. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  172. package/types/app/runtime/agent-runtime.d.ts +34 -3
  173. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  174. package/types/app/runtime/cue-runtime.d.ts +15 -3
  175. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  176. package/types/app/runtime/doing.d.ts +17 -0
  177. package/types/app/runtime/doing.d.ts.map +1 -1
  178. package/types/app/runtime/operation-owners.d.ts +16 -26
  179. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  180. package/types/app/runtime/workflow-contract.d.ts +21 -6
  181. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  182. package/types/app/transport/http-client.d.ts +9 -0
  183. package/types/app/transport/http-client.d.ts.map +1 -1
  184. package/types/app/transport/message-source.d.ts +6 -3
  185. package/types/app/transport/message-source.d.ts.map +1 -1
  186. package/types/app/transport/messengering.d.ts +10 -1
  187. package/types/app/transport/messengering.d.ts.map +1 -1
  188. package/types/app/witness/ingress.d.ts +1 -0
  189. package/types/app/witness/ingress.d.ts.map +1 -1
  190. package/types/app/witness/receipt-client.d.ts +19 -1
  191. package/types/app/witness/receipt-client.d.ts.map +1 -1
  192. package/types/app/witness/receipting.d.ts +24 -1
  193. package/types/app/witness/receipting.d.ts.map +1 -1
  194. package/types/cli/messaging/multisig.d.ts.map +1 -1
  195. package/types/core/cues.d.ts +53 -1
  196. package/types/core/cues.d.ts.map +1 -1
  197. package/types/core/errors.d.ts +32 -0
  198. package/types/core/errors.d.ts.map +1 -1
  199. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  200. package/types/core/kel/eventing.d.ts.map +1 -1
  201. package/types/core/kel/kever-decisions.d.ts +3 -1
  202. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  203. package/types/core/kel/kever.d.ts +7 -1
  204. package/types/core/kel/kever.d.ts.map +1 -1
  205. package/types/core/mailbox/kram-policy.d.ts +6 -0
  206. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  207. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  208. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  209. package/types/core/mailbox/kram-time.d.ts +26 -2
  210. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  211. package/types/core/mailbox/kraming.d.ts +14 -1
  212. package/types/core/mailbox/kraming.d.ts.map +1 -1
  213. package/types/core/routing.d.ts +3 -7
  214. package/types/core/routing.d.ts.map +1 -1
  215. package/types/core/scheming.d.ts.map +1 -1
  216. package/types/core/wire-fields.d.ts +56 -0
  217. package/types/core/wire-fields.d.ts.map +1 -0
  218. package/types/db/basing.d.ts.map +1 -1
  219. package/types/db/core/dber.d.ts +5 -0
  220. package/types/db/core/dber.d.ts.map +1 -1
  221. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  222. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  223. package/types/db/core/lmdber.d.ts +6 -0
  224. package/types/db/core/lmdber.d.ts.map +1 -1
  225. package/types/db/core/memstore.d.ts +2 -0
  226. package/types/db/core/memstore.d.ts.map +1 -1
  227. package/types/db/core/store-owner.d.ts +2 -0
  228. package/types/db/core/store-owner.d.ts.map +1 -1
  229. package/types/db/core/webdber.d.ts +32 -6
  230. package/types/db/core/webdber.d.ts.map +1 -1
  231. package/types/db/exchange/exchange-records.d.ts +4 -0
  232. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  233. package/types/db/mailbox/mailboxing.d.ts +10 -1
  234. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  235. package/types/db/subing.d.ts +2 -0
  236. package/types/db/subing.d.ts.map +1 -1
  237. package/types/runtime/index.d.ts +20 -14
  238. package/types/runtime/index.d.ts.map +1 -1
  239. package/types/time/mod.d.ts +2 -0
  240. package/types/time/mod.d.ts.map +1 -1
  241. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  242. package/types/vdr/eventing.d.ts.map +1 -1
  243. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  244. package/types/version.d.ts +2 -2
  245. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  246. package/types/app/runtime/signer-conditions.d.ts +0 -32
  247. package/types/app/runtime/signer-conditions.d.ts.map +0 -1
@@ -2,11 +2,12 @@
2
2
  import { b } from "cesr-ts";
3
3
  import { removeKramFactors } from "../../db/mailbox/kram-factors.js";
4
4
  import { timestampMicros } from "../../time/mod.js";
5
+ import { consoleLogger } from "../logger.js";
5
6
  import { TxnMsgCacheRecord } from "../records.js";
6
7
  import { kramCacheValues } from "./kram-time.js";
7
8
  /** Prune stored windows, then prove orphan XIDs with restartable finite existence scans. */
8
9
  export class KramPruner {
9
- constructor(db, policy) {
10
+ constructor(db, policy, logger = consoleLogger) {
10
11
  Object.defineProperty(this, "db", {
11
12
  enumerable: true,
12
13
  configurable: true,
@@ -19,6 +20,12 @@ export class KramPruner {
19
20
  writable: true,
20
21
  value: policy
21
22
  });
23
+ Object.defineProperty(this, "logger", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: logger
28
+ });
22
29
  Object.defineProperty(this, "bound", {
23
30
  enumerable: true,
24
31
  configurable: true,
@@ -95,7 +102,14 @@ export class KramPruner {
95
102
  throw new Error("Invalid KRAM pruning receiver clock");
96
103
  if (!Number.isSafeInteger(maxRows) || maxRows < 1)
97
104
  throw new RangeError("Invalid KRAM pruning batch");
98
- const result = { kind: "pruned", visited: 0, removed: 0, openers: 0, passComplete: false };
105
+ const result = {
106
+ kind: "pruned",
107
+ visited: 0,
108
+ removed: 0,
109
+ openers: 0,
110
+ partials: 0,
111
+ passComplete: false,
112
+ };
99
113
  if (!this.db.opened)
100
114
  throw new Error("KRAM pruning database is closed");
101
115
  if (this.bound !== this.db.msgc.sdb) {
@@ -114,15 +128,20 @@ export class KramPruner {
114
128
  continue;
115
129
  }
116
130
  const keys = this.keys(physical, this.phase + 2), table = this.phase ? this.db.tmsc : this.db.msgc;
117
- this.db.transaction(() => {
131
+ const partial = this.db.transaction(() => {
118
132
  const record = table.get(keys);
119
133
  if (!record || !this.expired(record, when))
120
- return;
134
+ return null;
121
135
  table.rem(keys);
122
- const partial = [keys[0], keys.at(-1)];
123
- removeKramFactors(this.db, partial);
136
+ const owner = [keys[0], keys.at(-1)], incomplete = this.db.pmkm.get(owner) !== null;
137
+ removeKramFactors(this.db, owner);
124
138
  result.removed++;
139
+ return incomplete ? owner : null;
125
140
  });
141
+ if (partial) {
142
+ result.partials++;
143
+ this.logger.warn("KRAM expired incomplete multi-signature message", { sender: partial[0], said: partial[1] });
144
+ }
126
145
  scan.start = this.after(physical);
127
146
  result.visited++;
128
147
  continue;
@@ -1,5 +1,16 @@
1
- import { timestampMicros } from "../../time/mod.js";
1
+ import { timestampFromMicros, timestampMicros } from "../../time/mod.js";
2
2
  import { MsgCacheRecord, TxnMsgCacheRecord } from "../records.js";
3
+ import { readDater, readDiger } from "../wire-fields.js";
4
+ /**
5
+ * Whether a drop reason means an authenticated message arrived too late or outside a live exchange.
6
+ *
7
+ * `timestamp`, `exchangeId`, `replay` and `authentication` are reachable by anyone who can post bytes (see
8
+ * kram-staging.ts), so callers that report or signal a refusal to anyone beyond the log must restrict themselves to
9
+ * these.
10
+ */
11
+ export function isKramLateReason(reason) {
12
+ return reason === "timeliness" || reason === "lifetime" || reason === "unknownThread";
13
+ }
3
14
  /** Validate durable cache values without turning corruption into remote refusal. */
4
15
  export function kramCacheValues(cache) {
5
16
  const { mdt, d, ml, pml, xl, pxl } = cache;
@@ -10,9 +21,19 @@ export function kramCacheValues(cache) {
10
21
  throw new Error("Corrupt KRAM timeliness cache");
11
22
  return { mdt: mdt, d: d, ml: ml, pml: pml, xl: xl, pxl: pxl };
12
23
  }
24
+ /** Exchange id KRAM threads a message by: the `xip` SAID or the v2 `exn` `x` field; empty when it has none. */
25
+ export function kramExchangeId(body) {
26
+ const said = body.said;
27
+ return body.ilk === "xip" ? said ?? "" : body.ilk === "exn" && body.pvrsn.major >= 2 ? String(body.ked?.x ?? "") : "";
28
+ }
13
29
  /**
14
30
  * Inspect frozen message/xid routing and strict completed-message replay rules.
15
31
  * Only an incomplete multi-signature message retains partial state.
32
+ *
33
+ * The stamp and exchange id are remote-authored and become durable values: an exchange message's stamp can be stored
34
+ * as its thread opener (a Dater, which needs the fixed-width form), and the exchange id is a cache key part (a value
35
+ * that is not one qualified digest, such as one containing `.`, would make a key the pruner cannot split). Both are
36
+ * refused here, before any store write.
16
37
  */
17
38
  export function inspectKramReplay(db, body) {
18
39
  const mdt = timestampMicros(body.stamp);
@@ -22,7 +43,11 @@ export function inspectKramReplay(db, body) {
22
43
  if (!sender || !said)
23
44
  throw new Error("KRAM replay inspection requires sender and message SAID");
24
45
  const key = [sender, said];
25
- const xid = body.ilk === "xip" ? said : body.ilk === "exn" && body.pvrsn.major >= 2 ? String(body.ked?.x ?? "") : "";
46
+ const xid = kramExchangeId(body);
47
+ if (xid && readDiger(xid)?.qb64 !== xid)
48
+ return { kind: "drop", reason: "exchangeId" };
49
+ if (xid && !readDater(body.stamp))
50
+ return { kind: "drop", reason: "timestamp" };
26
51
  const cacheKey = xid ? [sender, xid, said] : key;
27
52
  const cache = xid ? db.tmsc.get(cacheKey) : db.msgc.get(cacheKey);
28
53
  const partial = db.pmkm.get(key) !== null;
@@ -54,8 +79,9 @@ export function evaluateKramTime(db, policy, inspection, authentication, receive
54
79
  else {
55
80
  const type = policy.cacheType(body.ilk, body.route), multi = authentication.kind === "multi";
56
81
  const ml = multi ? type.ll : type.sl, pml = multi ? type.pll : type.psl;
57
- if (!within(mdt, receipt - (BigInt(type.d) + BigInt(ml)) * 1000n, receipt + BigInt(type.d) * 1000n)) {
58
- return { kind: "drop", reason: "timeliness" };
82
+ const reach = (BigInt(type.d) + BigInt(ml)) * 1000n;
83
+ if (!within(mdt, receipt - reach, receipt + BigInt(type.d) * 1000n)) {
84
+ return { kind: "drop", reason: "timeliness", deadline: timestampFromMicros(mdt + reach) };
59
85
  }
60
86
  values = { mdt: body.stamp, d: type.d, ml, pml, xl: type.xl, pxl: type.pxl };
61
87
  }
@@ -79,8 +105,10 @@ export function evaluateKramTime(db, policy, inspection, authentication, receive
79
105
  const opener = timestampMicros(xdt);
80
106
  if (opener === null)
81
107
  throw new Error("Corrupt KRAM transaction opener");
82
- if (!cache && !within(mdt, opener, opener + BigInt(values.xl) * 1000n))
83
- return { kind: "drop", reason: "lifetime" };
108
+ const closing = opener + BigInt(values.xl) * 1000n;
109
+ if (!cache && !within(mdt, opener, closing)) {
110
+ return { kind: "drop", reason: "lifetime", deadline: timestampFromMicros(closing) };
111
+ }
84
112
  const complete = authentication.kind !== "multi" || authentication.complete;
85
113
  const rdt = cache instanceof TxnMsgCacheRecord && cache.rdt ? cache.rdt : complete ? receiverTime : "";
86
114
  return {
@@ -1,19 +1,27 @@
1
1
  import { makeNowIso8601, timestampMicros } from "../../time/mod.js";
2
2
  import { Deck } from "../deck.js";
3
+ import { consoleLogger } from "../logger.js";
3
4
  import { KramPolicy } from "./kram-policy.js";
4
5
  import { KramPruner } from "./kram-pruning.js";
5
6
  import { stageKram } from "./kram-staging.js";
7
+ import { isKramLateReason, kramExchangeId } from "./kram-time.js";
6
8
  /** Explicit receiver-owned cache and exemption configuration. */
7
9
  export { DEFAULT_KRAM_CACHES } from "./kram-policy.js";
8
10
  /** Optional receiver intake returns ordinary decisions without owning parser input. */
9
11
  export class Kramer {
10
- constructor(db, { config = {}, cues = new Deck(), now = Date.now } = {}) {
12
+ constructor(db, { config = {}, cues = new Deck(), now = Date.now } = {}, logger = consoleLogger) {
11
13
  Object.defineProperty(this, "db", {
12
14
  enumerable: true,
13
15
  configurable: true,
14
16
  writable: true,
15
17
  value: db
16
18
  });
19
+ Object.defineProperty(this, "logger", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: logger
24
+ });
17
25
  /** Shared runtime cues, or the standalone caller's drainable receiver-state notifications. */
18
26
  Object.defineProperty(this, "cues", {
19
27
  enumerable: true,
@@ -42,7 +50,7 @@ export class Kramer {
42
50
  this.now = now;
43
51
  this.policy = new KramPolicy(db, config, now);
44
52
  this.cues = cues;
45
- this.pruner = new KramPruner(db, this.policy);
53
+ this.pruner = new KramPruner(db, this.policy, logger);
46
54
  }
47
55
  /** Decide intake synchronously; protocol dispatch consumes the returned evidence. */
48
56
  intake(envelope) {
@@ -64,8 +72,56 @@ export class Kramer {
64
72
  q: result.sn === null ? {} : { s: result.sn.toString(16) },
65
73
  });
66
74
  }
75
+ if (result.kind === "drop") {
76
+ this.report(envelope, result);
77
+ const cue = this.refusedCue(envelope, result);
78
+ if (cue)
79
+ this.cues.push(cue);
80
+ }
67
81
  return result;
68
82
  }
83
+ /**
84
+ * Describe a refusal the application may want to hear about, or null. Staging has already committed. A local
85
+ * caller receives its own refusal, and only the reasons KRAM decides after authentication are safe to signal:
86
+ * `timestamp`, `replay` and `authentication` are reachable by anyone who can post bytes.
87
+ */
88
+ refusedCue(envelope, refusal) {
89
+ if (envelope.local || !isKramLateReason(refusal.reason))
90
+ return null;
91
+ const { serder } = envelope;
92
+ const xid = kramExchangeId(serder), deadline = "deadline" in refusal ? refusal.deadline : undefined;
93
+ return {
94
+ kin: "kramRefused",
95
+ said: serder.said,
96
+ sender: serder.pre,
97
+ ilk: serder.ilk,
98
+ route: serder.route,
99
+ reason: refusal.reason,
100
+ stamp: serder.stamp,
101
+ ...(deadline === undefined ? {} : { deadline }),
102
+ ...(xid ? { xid } : {}),
103
+ };
104
+ }
105
+ /**
106
+ * Report one refused message. Replay is routine (every duplicate delivery lands here), so it logs at debug;
107
+ * the other reasons mean a sender or clock is misbehaving and log at warn. Dispatch does not log again.
108
+ */
109
+ report(envelope, refusal) {
110
+ const { serder } = envelope;
111
+ const fields = {
112
+ said: serder.said,
113
+ sender: serder.pre,
114
+ ilk: serder.ilk,
115
+ route: serder.route,
116
+ reason: refusal.reason,
117
+ stamp: serder.stamp,
118
+ deadline: "deadline" in refusal ? refusal.deadline : undefined,
119
+ };
120
+ if (refusal.reason === "replay")
121
+ this.logger.debug("KRAM refused message", fields);
122
+ else
123
+ this.logger.warn("KRAM refused message", fields);
124
+ }
69
125
  /** Reconfigure cache windows through the durable policy transition facility. */
70
126
  changeConfig(caches, delay) {
71
127
  this.policy.change(caches, delay);
@@ -1,11 +1,12 @@
1
- import { Dater, Diger, NumberPrimitive, Prefixer, Siger } from "cesr-ts";
2
- import { encodeDateTimeToDater, timestampMicros } from "../time/mod.js";
1
+ import { Diger, NumberPrimitive, Prefixer, Siger } from "cesr-ts";
2
+ import { timestampMicros } from "../time/mod.js";
3
3
  import { Deck } from "./deck.js";
4
4
  import { TransIdxSigGroup } from "./dispatch.js";
5
5
  import { ValidationError } from "./errors.js";
6
6
  import { acceptEscrow, dropEscrow, keepEscrow, logEscrowDecision, } from "./kel/kever-decisions.js";
7
7
  import { EndpointRecord, LocationRecord } from "./records.js";
8
8
  import { isRole } from "./roles.js";
9
+ import { readDater, readPrefixer, readUrl } from "./wire-fields.js";
9
10
  /** Builder for one accepted live reply decision. */
10
11
  export function acceptReplyDecision() {
11
12
  return { kind: "accept" };
@@ -276,6 +277,7 @@ Object.defineProperty(Router, "defaultResourceFunc", {
276
277
  * - persist accepted reply artifacts into `sdts.`, `rpys.`, `scgs.`, `ssgs.`
277
278
  * - escrow partially verifiable transferable replies into `rpes.`
278
279
  * - emit `query` cues when missing signer state blocks verification
280
+ * - emit an `endRoleSaved` cue after an accepted `/end/role` reply commits (TypeScript-only; KERIpy has no consumer)
279
281
  *
280
282
  * Current `keri-ts` differences:
281
283
  * - only the Gate E bootstrap reply families are currently routed
@@ -321,17 +323,6 @@ export class Revery {
321
323
  writable: true,
322
324
  value: void 0
323
325
  });
324
- /**
325
- * Told after an `/end/role` reply is accepted, with the controller, role, endpoint and whether the role is
326
- * allowed. The runtime uses it to have the mailbox poller and KEL publisher look at a controller whose mailbox
327
- * role just changed, rather than at their next sweep.
328
- */
329
- Object.defineProperty(this, "onEndRole", {
330
- enumerable: true,
331
- configurable: true,
332
- writable: true,
333
- value: void 0
334
- });
335
326
  this.db = db;
336
327
  this.rtr = rtr ?? new Router();
337
328
  this.cues = cues ?? new Deck();
@@ -404,11 +395,10 @@ export class Revery {
404
395
  // Same-call groups see the last staged pin exactly as they previously saw the durable write.
405
396
  const staged = new Map();
406
397
  const groupKey = (pre, snh, said) => `${pre}.${snh}.${said}`;
407
- const daterText = args.serder.ked?.dt;
408
- if (typeof daterText !== "string") {
409
- throw new ValidationError("Reply message is missing 'dt'.");
398
+ const dater = readDater(args.serder.ked?.dt);
399
+ if (!dater) {
400
+ throw new ValidationError("Reply message is missing a valid 'dt'.");
410
401
  }
411
- const dater = new Dater({ qb64: encodeDateTimeToDater(daterText) });
412
402
  const odater = args.osaider ? this.db.sdts.get([args.osaider.qb64]) : null;
413
403
  let pending = false;
414
404
  const escrow = (material) => {
@@ -802,7 +792,8 @@ export class BasicReplyRouteHandler {
802
792
  * - derives the BADA route base `/end/role`
803
793
  * - applies same-SAID idempotence before acceptance
804
794
  * - evaluates proof and updates proof/`eans.`/`ends.` in its own transaction
805
- * - publishes queries only after that commit; caller-owned writers are refused
795
+ * - publishes queries, then an `endRoleSaved` cue for an accepted reply, only after that commit; caller-owned
796
+ * writers are refused
806
797
  */
807
798
  processReplyEndRole(args) {
808
799
  const allowed = args.route.startsWith("/end/role/add")
@@ -817,11 +808,11 @@ export class BasicReplyRouteHandler {
817
808
  if (!data || typeof data !== "object" || Array.isArray(data)) {
818
809
  throw new ValidationError("Missing one of cid/role/eid in /end/role reply.");
819
810
  }
820
- const cid = "cid" in data && typeof data.cid === "string" ? new Prefixer({ qb64: data.cid }).qb64 : null;
811
+ const cid = "cid" in data ? readPrefixer(data.cid)?.qb64 ?? null : null;
821
812
  const role = "role" in data && typeof data.role === "string" ? data.role : null;
822
- const eid = "eid" in data && typeof data.eid === "string" ? new Prefixer({ qb64: data.eid }).qb64 : null;
813
+ const eid = "eid" in data ? readPrefixer(data.eid)?.qb64 ?? null : null;
823
814
  if (!cid || !role || !eid) {
824
- throw new ValidationError("Missing one of cid/role/eid in /end/role reply.");
815
+ throw new ValidationError("Missing or malformed one of cid/role/eid in /end/role reply.");
825
816
  }
826
817
  if (!isRole(role)) {
827
818
  throw new ValidationError(`Invalid endpoint role ${role}.`);
@@ -855,7 +846,7 @@ export class BasicReplyRouteHandler {
855
846
  if (queryCues.length)
856
847
  this.rvy.cues.extend(queryCues);
857
848
  if (decision.kind === "accept")
858
- this.rvy.onEndRole?.(cid, role, eid, allowed);
849
+ this.rvy.cues.push({ kin: "endRoleSaved", cid, role, eid, allowed });
859
850
  return decision;
860
851
  }
861
852
  /**
@@ -877,14 +868,16 @@ export class BasicReplyRouteHandler {
877
868
  if (!data || typeof data !== "object" || Array.isArray(data)) {
878
869
  throw new ValidationError("Missing one of eid/scheme/url in /loc/scheme reply.");
879
870
  }
880
- const eid = "eid" in data && typeof data.eid === "string" ? new Prefixer({ qb64: data.eid }).qb64 : null;
871
+ const eid = "eid" in data ? readPrefixer(data.eid)?.qb64 ?? null : null;
881
872
  const scheme = "scheme" in data && typeof data.scheme === "string" ? data.scheme : null;
882
873
  const url = "url" in data && typeof data.url === "string" ? data.url : null;
883
874
  if (!eid || !scheme || url === null) {
884
- throw new ValidationError("Missing one of eid/scheme/url in /loc/scheme reply.");
875
+ throw new ValidationError("Missing or malformed one of eid/scheme/url in /loc/scheme reply.");
885
876
  }
886
877
  if (url) {
887
- const parsed = new URL(url);
878
+ const parsed = readUrl(url);
879
+ if (!parsed)
880
+ throw new ValidationError(`Unparseable URL ${url} in /loc/scheme reply.`);
888
881
  if (parsed.protocol && parsed.protocol.slice(0, -1) !== scheme) {
889
882
  throw new ValidationError(`URL ${url} does not match declared scheme ${scheme}.`);
890
883
  }
@@ -129,14 +129,21 @@ function parseSchemaJson(raw) {
129
129
  }
130
130
  return parsed;
131
131
  }
132
+ /**
133
+ * Decode credential data for validation. The bytes are a remote credential's body, which need not be JSON (a CBOR or
134
+ * MGPK ACDC); data that does not decode fails validation as a `ValidationError`, like KERIpy's `json.loads` failure.
135
+ */
132
136
  function parseSchemaData(raw) {
133
- if (raw instanceof Uint8Array) {
134
- return JSON.parse(new TextDecoder().decode(raw));
137
+ if (!(raw instanceof Uint8Array) && typeof raw !== "string")
138
+ return raw;
139
+ try {
140
+ return JSON.parse(typeof raw === "string" ? raw : new TextDecoder().decode(raw));
135
141
  }
136
- if (typeof raw === "string") {
137
- return JSON.parse(raw);
142
+ catch (error) {
143
+ if (!(error instanceof SyntaxError))
144
+ throw error;
145
+ throw new ValidationError(`Credential data is not JSON: ${error.message}`);
138
146
  }
139
- return raw;
140
147
  }
141
148
  function verifySchemaSaid(sed, kind) {
142
149
  const id = sed.$id;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Guarded readers for the field values of remote messages.
3
+ *
4
+ * A remote message can parse and verify its SAID while a field value is still not a valid value for its type: a
5
+ * non-hex or over-wide sequence number, a key that is not a verification key, a next-key digest that is not a digest,
6
+ * a prefix that is not a prefix. The cesr accessors and primitive constructors throw on such values, and a throw out
7
+ * of a processor ends its loop (the mailbox poller, the escrow doer, HTTP ingress). Processor boundaries therefore read
8
+ * remote field values through these readers and turn a null into their own typed refusal (ADR-0005) before any escrow
9
+ * decision or store write: `Kevery.decideEvent` and `processReceipt`, the reply route handlers and `Revery`'s
10
+ * datetime read, `Exchanger` admission and `queryEnvelopeFromDispatch`. Accepted KEL events are remote-authored too:
11
+ * their raw `a` seals are read through `readEventSeal` wherever an anchor is searched.
12
+ *
13
+ * Only the cesr codec's own malformed-input taxonomy (`ParserError` and its subclasses) is read as malformed input, and
14
+ * only while constructing a primitive from a string the message carried. That construction reads no store, so a
15
+ * `ParserError` there cannot be corrupt durable state or I/O; every other error propagates. Numbers are checked as
16
+ * text before any `BigInt` conversion, so cesr's `SyntaxError`/`RangeError` for bad hex is never relied on.
17
+ */
18
+ import { Dater, Diger, Ilks, NumberPrimitive, ParserError, Prefixer, SealEvent, Verfer, } from "cesr-ts";
19
+ import { encodeDateTimeToDater } from "../time/mod.js";
20
+ /** Construct `Type` from qualified text, or null when the text is not a valid value of that type. */
21
+ function readQualified(Type, value) {
22
+ if (typeof value !== "string" || !value)
23
+ return null;
24
+ try {
25
+ return new Type({ qb64: value });
26
+ }
27
+ catch (error) {
28
+ if (error instanceof ParserError)
29
+ return null;
30
+ throw error;
31
+ }
32
+ }
33
+ /** Read an identifier prefix (`i`, `di`, `cid`, `eid`, ...), or null when it is not one. */
34
+ export function readPrefixer(value) {
35
+ return readQualified(Prefixer, value);
36
+ }
37
+ /** Read a digest (`d` of a key state notice, a next-key digest), or null when it is not one. */
38
+ export function readDiger(value) {
39
+ return readQualified(Diger, value);
40
+ }
41
+ /** Read a verification key (a current key, a witness), or null when it is not one. */
42
+ export function readVerfer(value) {
43
+ return readQualified(Verfer, value);
44
+ }
45
+ /** Read a datetime (`dt`) as a Dater, or null when it does not encode as one. */
46
+ export function readDater(value) {
47
+ return typeof value === "string" ? readQualified(Dater, encodeDateTimeToDater(value)) : null;
48
+ }
49
+ /** Read a URL, or null when it does not parse. */
50
+ export function readUrl(value) {
51
+ return URL.canParse(value) ? new URL(value) : null;
52
+ }
53
+ /**
54
+ * Read a KERI ordinal: lower- or upper-case hex text, or a non-negative integer number or bigint, as cesr's numeric
55
+ * ladder accepts them. Null when it is none of those or exceeds `Number.MAX_SAFE_INTEGER`, the largest value the KEL
56
+ * processors can compare exactly as a number.
57
+ */
58
+ export function readOrdinal(value) {
59
+ let ordinal;
60
+ if (typeof value === "string") {
61
+ if (!/^[0-9a-f]+$/iu.test(value))
62
+ return null;
63
+ ordinal = BigInt(`0x${value}`);
64
+ }
65
+ else if (typeof value === "number") {
66
+ if (!Number.isSafeInteger(value) || value < 0)
67
+ return null;
68
+ ordinal = BigInt(value);
69
+ }
70
+ else if (typeof value === "bigint") {
71
+ ordinal = value;
72
+ }
73
+ else {
74
+ return null;
75
+ }
76
+ return ordinal >= 0n && ordinal <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(ordinal) : null;
77
+ }
78
+ /** Read a message's `s`, which cesr's `sner` reads only as hex text; null when absent or malformed. */
79
+ export function readSequenceNumber(serder) {
80
+ const sn = serder.ked?.s;
81
+ return typeof sn === "string" ? readOrdinal(sn) : null;
82
+ }
83
+ /**
84
+ * Name the first field of a KEL event whose value the key-state machine would fail to decode, or null when all decode.
85
+ *
86
+ * Covers what `Kever` and escrow persistence decode beyond `i` and `s` (which `decideEvent` reads first): current keys
87
+ * `k` and next-key digests `n` of establishment events, witnesses `b` and witness changes `br`/`ba`, the witness
88
+ * threshold `bt`, and the delegator `di` of a delegated inception. Thresholds (`kt`, `nt`) already decode to null and are
89
+ * rejected by `Kever`; raw seals in `a` are not decoded here.
90
+ */
91
+ export function kelEventFieldDefect(serder) {
92
+ const ked = serder.ked ?? {};
93
+ const ilk = serder.ilk;
94
+ const bad = (label, values, read) => {
95
+ const value = values.find((entry) => read(entry) === null);
96
+ return value === undefined ? null : `${label} ${JSON.stringify(value)}`;
97
+ };
98
+ return bad("key", serder.keys, readVerfer)
99
+ ?? bad("next key digest", serder.ndigs, readDiger)
100
+ ?? bad("witness", serder.backs, readVerfer)
101
+ ?? bad("witness cut", serder.cuts, readVerfer)
102
+ ?? bad("witness add", serder.adds, readVerfer)
103
+ ?? (ked.bt !== undefined && ked.bt !== null && readOrdinal(ked.bt) === null
104
+ ? `witness threshold ${JSON.stringify(ked.bt)}`
105
+ : null)
106
+ ?? (ilk === Ilks.dip && serder.delpre !== null && !readPrefixer(serder.delpre)
107
+ ? `delegator ${JSON.stringify(serder.delpre)}`
108
+ : null);
109
+ }
110
+ /**
111
+ * Read one raw `a` entry as an event seal `{i, s, d}`, or null when it is another seal shape or carries a value that
112
+ * does not decode.
113
+ *
114
+ * An accepted event's seals are not value-checked at acceptance (KERIpy keeps them as raw dicts), so any identifier can
115
+ * anchor junk in its own KEL. Anchor searches (delegation approval, sealing-event lookup) therefore read each entry
116
+ * through this reader and skip a null instead of throwing. KERIpy filters by the same field shape
117
+ * (`tuple(seal) == SealEvent._fields`) and compares raw strings, so a malformed value can never match a real event.
118
+ */
119
+ export function readEventSeal(value) {
120
+ if (!SealEvent.isSad(value))
121
+ return null;
122
+ const pre = readPrefixer(value.i);
123
+ const sn = readOrdinal(value.s);
124
+ const said = readDiger(value.d);
125
+ return pre && sn !== null && said ? SealEvent.fromTuple([pre, NumberPrimitive.fromNum(BigInt(sn)), said]) : null;
126
+ }
@@ -1,11 +1,12 @@
1
1
  /** KERI event-log databaser built on `LMDBer` composition. */
2
- import { b, Cigar, Dater, Diger, Ilks, Labeler, Noncer, NumberPrimitive, NumDex, Prefixer, Saider, SealEvent, SerderKERI, Siger, t, Texter, Verfer, Verser, Vrsn_1_0, } from "cesr-ts";
2
+ import { b, Cigar, Dater, Diger, Ilks, Labeler, Noncer, NumberPrimitive, NumDex, Prefixer, Saider, SerderKERI, Siger, t, Texter, Verfer, Verser, Vrsn_1_0, } from "cesr-ts";
3
3
  import { action } from "effection";
4
4
  import { attachmentCounterPayloadQb64b, attachmentCounterQb64b, attachmentOrdinal, resolveAttachmentGvrsn, } from "../core/attachment-countering.js";
5
5
  import { DatabaseOperationError, ValidationError } from "../core/errors.js";
6
6
  import { Kever } from "../core/kel/kever.js";
7
7
  import { consoleLogger } from "../core/logger.js";
8
8
  import { CacheTypeRecord, EndpointRecord, EventSourceRecord, HabitatRecord, KelPublishedRecord, KeyStateRecord, LocationRecord, MsgCacheRecord, ObservedRecord, OobiRecord, RootSentRecord, TopicsRecord, TxnMsgCacheRecord, WellKnownAuthN, } from "../core/records.js";
9
+ import { readEventSeal } from "../core/wire-fields.js";
9
10
  import { encodeDateTimeToDater, makeNowIso8601 } from "../time/mod.js";
10
11
  import { dgKey, splitOnKey } from "./core/keys.js";
11
12
  import { StoreOwner } from "./core/store-owner.js";
@@ -29,19 +30,9 @@ function isEventSealRecord(value) {
29
30
  && value.s instanceof NumberPrimitive
30
31
  && value.d instanceof Diger;
31
32
  }
33
+ /** Accept an already-typed event seal, or read a raw `{i, s, d}` entry; null for other shapes or bad values. */
32
34
  function normalizeEventSeal(value) {
33
- if (isEventSealRecord(value)) {
34
- return value;
35
- }
36
- if (!SealEvent.isSad(value)) {
37
- return null;
38
- }
39
- try {
40
- return SealEvent.fromSad(value);
41
- }
42
- catch {
43
- return null;
44
- }
35
+ return isEventSealRecord(value) ? value : readEventSeal(value);
45
36
  }
46
37
  function eventSealsEqual(left, right) {
47
38
  return left.i.qb64 === right.i.qb64
@@ -25,8 +25,9 @@
25
25
  *
26
26
  * Another tab's upgrade or delete fires `versionchange` here. The connection
27
27
  * closes at once so that tab is never blocked, and this adapter is then lost:
28
- * its next `load` or `write` rejects, which latches the queue, because writing
29
- * on after another context deleted the database would break prefix durability.
28
+ * its next `load` or `write` rejects, and so does every retry of it (a write never
29
+ * reconnects), which latches the queue, because writing on after another context
30
+ * deleted the database would break prefix durability.
30
31
  * Recovery is a new open, whose `meta` reconnects. Single-writer exclusion is
31
32
  * the composition's job, not this adapter's.
32
33
  *
@@ -254,6 +254,13 @@ export class LMDBer extends BaseDBer {
254
254
  writable: true,
255
255
  value: false
256
256
  });
257
+ /** Write attempts made through this handle; a process-local counter, so reading it touches no store. */
258
+ Object.defineProperty(this, "commits", {
259
+ enumerable: true,
260
+ configurable: true,
261
+ writable: true,
262
+ value: 0
263
+ });
257
264
  this.defaults = { ...LMDBER_DEFAULTS, ...defaults };
258
265
  this.logger = options.logger ?? consoleLogger;
259
266
  this.configuredMapSize = options.mapSize;
@@ -482,7 +489,7 @@ export class LMDBer extends BaseDBer {
482
489
  setVer(val) {
483
490
  const env = this.requireEnv();
484
491
  try {
485
- env.transactionSync(() => {
492
+ this.writing(env).transactionSync(() => {
486
493
  env.putSync(b("__version__"), b(val));
487
494
  });
488
495
  }
@@ -521,6 +528,7 @@ export class LMDBer extends BaseDBer {
521
528
  }
522
529
  /** Run `fn` in one native synchronous write transaction; nesting is a child transaction. */
523
530
  transaction(fn) {
531
+ // A transaction is not itself a write: the puts and removes inside it count, so one that writes nothing does not.
524
532
  return this.requireEnv().transactionSync(fn);
525
533
  }
526
534
  /**
@@ -538,6 +546,15 @@ export class LMDBer extends BaseDBer {
538
546
  yield { key, value: copyDbBytes(entry.value) };
539
547
  }
540
548
  }
549
+ /** Count one write attempt through this handle and return the environment to write through. */
550
+ writing(env) {
551
+ this.commits++;
552
+ return env;
553
+ }
554
+ /** Counts this handle's own writes without reading the store; other processes' commits are not seen. */
555
+ get writeActivity() {
556
+ return this.commits;
557
+ }
541
558
  /** LMDB's committed transaction ID, which also counts other processes' commits. */
542
559
  get writeVersion() {
543
560
  const version = this.requireEnv().getStats().lastTxnId;
@@ -610,9 +627,11 @@ export class LMDBer extends BaseDBer {
610
627
  }
611
628
  putRow(table, key, val) {
612
629
  assertRowFits(table, key, val);
630
+ this.commits++;
613
631
  lmdbTable(table).putSync(key, val);
614
632
  }
615
633
  removeRow(table, key, val) {
634
+ this.commits++;
616
635
  return val === undefined ? lmdbTable(table).removeSync(key) : lmdbTable(table).removeSync(key, val);
617
636
  }
618
637
  /*
@@ -632,7 +651,7 @@ export class LMDBer extends BaseDBer {
632
651
  const db = lmdbTable(table);
633
652
  const env = this.requireEnv();
634
653
  try {
635
- const result = env.transactionSync(() => {
654
+ const result = this.writing(env).transactionSync(() => {
636
655
  const existing = db.get(key);
637
656
  if (existing !== null && existing !== undefined) {
638
657
  return false;
@@ -657,7 +676,7 @@ export class LMDBer extends BaseDBer {
657
676
  const db = lmdbTable(table);
658
677
  const env = this.requireEnv();
659
678
  try {
660
- env.transactionSync(() => {
679
+ this.writing(env).transactionSync(() => {
661
680
  assertRowFits(db, key, val);
662
681
  db.put(key, val);
663
682
  });
@@ -700,7 +719,7 @@ export class LMDBer extends BaseDBer {
700
719
  const db = lmdbTable(table);
701
720
  const env = this.requireEnv();
702
721
  try {
703
- const result = env.transactionSync(() => {
722
+ const result = this.writing(env).transactionSync(() => {
704
723
  if (val !== undefined) {
705
724
  // An empty key must still fail, but an over-long one can only be absent.
706
725
  if (key.length > MAX_KEY_BYTES)
@@ -809,7 +828,7 @@ export class LMDBer extends BaseDBer {
809
828
  if (keys.length === 0) {
810
829
  return false;
811
830
  }
812
- env.transactionSync(() => {
831
+ this.writing(env).transactionSync(() => {
813
832
  for (const key of keys) {
814
833
  db.remove(key);
815
834
  }
@@ -870,7 +889,7 @@ export class LMDBer extends BaseDBer {
870
889
  let result = true;
871
890
  const existing = new Set(this.getVals(db, key).map((val) => bytesHex(val)));
872
891
  const env = this.requireEnv();
873
- env.transactionSync(() => {
892
+ this.writing(env).transactionSync(() => {
874
893
  for (const val of vals) {
875
894
  const vh = bytesHex(val);
876
895
  if (existing.has(vh)) {
@@ -173,6 +173,10 @@ export class MemStore extends BaseDBer {
173
173
  this.assertOpen();
174
174
  return __classPrivateFieldGet(this, _MemStore_writeVersion, "f");
175
175
  }
176
+ /** Same count as {@link writeVersion}; in memory, so reading it is free. */
177
+ get writeActivity() {
178
+ return __classPrivateFieldGet(this, _MemStore_writeVersion, "f");
179
+ }
176
180
  /** Open or reopen the contents. A readonly open of never-created contents reports `false`. */
177
181
  *reopen(options = {}) {
178
182
  this.readonly = options.readonly ?? this.readonly;