metered-protocol 1.0.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.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/contracts/metered.ag +135 -0
  4. package/contracts/metered_ag.tests.json +1226 -0
  5. package/contracts/metered_session.sil +237 -0
  6. package/contracts/metered_session.tests.json +1113 -0
  7. package/dist/contracts/metered.ag +135 -0
  8. package/dist/contracts/metered_ag.tests.json +1226 -0
  9. package/dist/contracts/metered_session.sil +237 -0
  10. package/dist/contracts/metered_session.tests.json +1113 -0
  11. package/dist/src/bias.d.ts +48 -0
  12. package/dist/src/bias.d.ts.map +1 -0
  13. package/dist/src/bias.js +43 -0
  14. package/dist/src/bias.js.map +1 -0
  15. package/dist/src/checkpoint.d.ts +57 -0
  16. package/dist/src/checkpoint.d.ts.map +1 -0
  17. package/dist/src/checkpoint.js +84 -0
  18. package/dist/src/checkpoint.js.map +1 -0
  19. package/dist/src/deadline.d.ts +71 -0
  20. package/dist/src/deadline.d.ts.map +1 -0
  21. package/dist/src/deadline.js +77 -0
  22. package/dist/src/deadline.js.map +1 -0
  23. package/dist/src/encoding.d.ts +72 -0
  24. package/dist/src/encoding.d.ts.map +1 -0
  25. package/dist/src/encoding.js +187 -0
  26. package/dist/src/encoding.js.map +1 -0
  27. package/dist/src/history.d.ts +33 -0
  28. package/dist/src/history.d.ts.map +1 -0
  29. package/dist/src/history.js +38 -0
  30. package/dist/src/history.js.map +1 -0
  31. package/dist/src/http/buyer.d.ts +57 -0
  32. package/dist/src/http/buyer.d.ts.map +1 -0
  33. package/dist/src/http/buyer.js +151 -0
  34. package/dist/src/http/buyer.js.map +1 -0
  35. package/dist/src/http/client.d.ts +55 -0
  36. package/dist/src/http/client.d.ts.map +1 -0
  37. package/dist/src/http/client.js +83 -0
  38. package/dist/src/http/client.js.map +1 -0
  39. package/dist/src/http/harness.d.ts +18 -0
  40. package/dist/src/http/harness.d.ts.map +1 -0
  41. package/dist/src/http/harness.js +16 -0
  42. package/dist/src/http/harness.js.map +1 -0
  43. package/dist/src/http/protocol.d.ts +84 -0
  44. package/dist/src/http/protocol.d.ts.map +1 -0
  45. package/dist/src/http/protocol.js +22 -0
  46. package/dist/src/http/protocol.js.map +1 -0
  47. package/dist/src/http/provider.d.ts +123 -0
  48. package/dist/src/http/provider.d.ts.map +1 -0
  49. package/dist/src/http/provider.js +197 -0
  50. package/dist/src/http/provider.js.map +1 -0
  51. package/dist/src/http/serve.d.ts +34 -0
  52. package/dist/src/http/serve.d.ts.map +1 -0
  53. package/dist/src/http/serve.js +171 -0
  54. package/dist/src/http/serve.js.map +1 -0
  55. package/dist/src/http/service.d.ts +86 -0
  56. package/dist/src/http/service.d.ts.map +1 -0
  57. package/dist/src/http/service.js +161 -0
  58. package/dist/src/http/service.js.map +1 -0
  59. package/dist/src/index.d.ts +68 -0
  60. package/dist/src/index.d.ts.map +1 -0
  61. package/dist/src/index.js +62 -0
  62. package/dist/src/index.js.map +1 -0
  63. package/dist/src/meter.d.ts +39 -0
  64. package/dist/src/meter.d.ts.map +1 -0
  65. package/dist/src/meter.js +100 -0
  66. package/dist/src/meter.js.map +1 -0
  67. package/dist/src/offer.d.ts +6 -0
  68. package/dist/src/offer.d.ts.map +1 -0
  69. package/dist/src/offer.js +92 -0
  70. package/dist/src/offer.js.map +1 -0
  71. package/dist/src/reconcile.d.ts +18 -0
  72. package/dist/src/reconcile.d.ts.map +1 -0
  73. package/dist/src/reconcile.js +101 -0
  74. package/dist/src/reconcile.js.map +1 -0
  75. package/dist/src/reservation.d.ts +51 -0
  76. package/dist/src/reservation.d.ts.map +1 -0
  77. package/dist/src/reservation.js +124 -0
  78. package/dist/src/reservation.js.map +1 -0
  79. package/dist/src/session.d.ts +40 -0
  80. package/dist/src/session.d.ts.map +1 -0
  81. package/dist/src/session.js +92 -0
  82. package/dist/src/session.js.map +1 -0
  83. package/dist/src/signer.d.ts +31 -0
  84. package/dist/src/signer.d.ts.map +1 -0
  85. package/dist/src/signer.js +57 -0
  86. package/dist/src/signer.js.map +1 -0
  87. package/dist/src/store.d.ts +30 -0
  88. package/dist/src/store.d.ts.map +1 -0
  89. package/dist/src/store.js +120 -0
  90. package/dist/src/store.js.map +1 -0
  91. package/dist/src/types.d.ts +80 -0
  92. package/dist/src/types.d.ts.map +1 -0
  93. package/dist/src/types.js +9 -0
  94. package/dist/src/types.js.map +1 -0
  95. package/dist/tools/anchor-cli.d.ts +2 -0
  96. package/dist/tools/anchor-cli.d.ts.map +1 -0
  97. package/dist/tools/anchor-cli.js +101 -0
  98. package/dist/tools/anchor-cli.js.map +1 -0
  99. package/dist/tools/anchor.d.ts +68 -0
  100. package/dist/tools/anchor.d.ts.map +1 -0
  101. package/dist/tools/anchor.js +136 -0
  102. package/dist/tools/anchor.js.map +1 -0
  103. package/dist/tools/chain.d.ts +18 -0
  104. package/dist/tools/chain.d.ts.map +1 -0
  105. package/dist/tools/chain.js +18 -0
  106. package/dist/tools/chain.js.map +1 -0
  107. package/dist/tools/conformance.d.ts +2 -0
  108. package/dist/tools/conformance.d.ts.map +1 -0
  109. package/dist/tools/conformance.js +220 -0
  110. package/dist/tools/conformance.js.map +1 -0
  111. package/dist/tools/contracts.d.ts +2 -0
  112. package/dist/tools/contracts.d.ts.map +1 -0
  113. package/dist/tools/contracts.js +68 -0
  114. package/dist/tools/contracts.js.map +1 -0
  115. package/dist/tools/covenant-binding.d.ts +40 -0
  116. package/dist/tools/covenant-binding.d.ts.map +1 -0
  117. package/dist/tools/covenant-binding.js +60 -0
  118. package/dist/tools/covenant-binding.js.map +1 -0
  119. package/dist/tools/covenant-profile.d.ts +41 -0
  120. package/dist/tools/covenant-profile.d.ts.map +1 -0
  121. package/dist/tools/covenant-profile.js +74 -0
  122. package/dist/tools/covenant-profile.js.map +1 -0
  123. package/dist/tools/covenant.d.ts +30 -0
  124. package/dist/tools/covenant.d.ts.map +1 -0
  125. package/dist/tools/covenant.js +54 -0
  126. package/dist/tools/covenant.js.map +1 -0
  127. package/dist/tools/demo-report.d.ts +24 -0
  128. package/dist/tools/demo-report.d.ts.map +1 -0
  129. package/dist/tools/demo-report.js +40 -0
  130. package/dist/tools/demo-report.js.map +1 -0
  131. package/dist/tools/demo.d.ts +2 -0
  132. package/dist/tools/demo.d.ts.map +1 -0
  133. package/dist/tools/demo.js +204 -0
  134. package/dist/tools/demo.js.map +1 -0
  135. package/dist/tools/dust-map.d.ts +2 -0
  136. package/dist/tools/dust-map.d.ts.map +1 -0
  137. package/dist/tools/dust-map.js +63 -0
  138. package/dist/tools/dust-map.js.map +1 -0
  139. package/dist/tools/expire-cases.d.ts +4 -0
  140. package/dist/tools/expire-cases.d.ts.map +1 -0
  141. package/dist/tools/expire-cases.js +211 -0
  142. package/dist/tools/expire-cases.js.map +1 -0
  143. package/dist/tools/fee-check.d.ts +2 -0
  144. package/dist/tools/fee-check.d.ts.map +1 -0
  145. package/dist/tools/fee-check.js +127 -0
  146. package/dist/tools/fee-check.js.map +1 -0
  147. package/dist/tools/fixtures.d.ts +39 -0
  148. package/dist/tools/fixtures.d.ts.map +1 -0
  149. package/dist/tools/fixtures.js +31 -0
  150. package/dist/tools/fixtures.js.map +1 -0
  151. package/dist/tools/gen-contract-tests.d.ts +2 -0
  152. package/dist/tools/gen-contract-tests.d.ts.map +1 -0
  153. package/dist/tools/gen-contract-tests.js +170 -0
  154. package/dist/tools/gen-contract-tests.js.map +1 -0
  155. package/dist/tools/kaspa.d.ts +13 -0
  156. package/dist/tools/kaspa.d.ts.map +1 -0
  157. package/dist/tools/kaspa.js +76 -0
  158. package/dist/tools/kaspa.js.map +1 -0
  159. package/dist/tools/live-deadline.d.ts +2 -0
  160. package/dist/tools/live-deadline.d.ts.map +1 -0
  161. package/dist/tools/live-deadline.js +122 -0
  162. package/dist/tools/live-deadline.js.map +1 -0
  163. package/dist/tools/live-expire.d.ts +2 -0
  164. package/dist/tools/live-expire.d.ts.map +1 -0
  165. package/dist/tools/live-expire.js +127 -0
  166. package/dist/tools/live-expire.js.map +1 -0
  167. package/dist/tools/live-settle.d.ts +2 -0
  168. package/dist/tools/live-settle.d.ts.map +1 -0
  169. package/dist/tools/live-settle.js +226 -0
  170. package/dist/tools/live-settle.js.map +1 -0
  171. package/dist/tools/live-steps.d.ts +63 -0
  172. package/dist/tools/live-steps.d.ts.map +1 -0
  173. package/dist/tools/live-steps.js +155 -0
  174. package/dist/tools/live-steps.js.map +1 -0
  175. package/dist/tools/model.d.ts +32 -0
  176. package/dist/tools/model.d.ts.map +1 -0
  177. package/dist/tools/model.js +99 -0
  178. package/dist/tools/model.js.map +1 -0
  179. package/dist/tools/send.d.ts +2 -0
  180. package/dist/tools/send.d.ts.map +1 -0
  181. package/dist/tools/send.js +41 -0
  182. package/dist/tools/send.js.map +1 -0
  183. package/dist/tools/session-chain.d.ts +69 -0
  184. package/dist/tools/session-chain.d.ts.map +1 -0
  185. package/dist/tools/session-chain.js +113 -0
  186. package/dist/tools/session-chain.js.map +1 -0
  187. package/dist/tools/sighash.d.ts +9 -0
  188. package/dist/tools/sighash.d.ts.map +1 -0
  189. package/dist/tools/sighash.js +68 -0
  190. package/dist/tools/sighash.js.map +1 -0
  191. package/dist/tools/sigscript-check.d.ts +2 -0
  192. package/dist/tools/sigscript-check.d.ts.map +1 -0
  193. package/dist/tools/sigscript-check.js +113 -0
  194. package/dist/tools/sigscript-check.js.map +1 -0
  195. package/dist/tools/sigscript.d.ts +61 -0
  196. package/dist/tools/sigscript.d.ts.map +1 -0
  197. package/dist/tools/sigscript.js +70 -0
  198. package/dist/tools/sigscript.js.map +1 -0
  199. package/dist/tools/state-layout.d.ts +2 -0
  200. package/dist/tools/state-layout.d.ts.map +1 -0
  201. package/dist/tools/state-layout.js +57 -0
  202. package/dist/tools/state-layout.js.map +1 -0
  203. package/package.json +59 -0
  204. package/spec/CONFORMANCE.md +80 -0
  205. package/spec/SPEC.md +821 -0
  206. package/spec/conformance-vectors.json +677 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * SPEC.md §6 -- the meters, and resolving the one an Offer names.
3
+ *
4
+ * A METER TURNS DELIVERED CONTENT INTO A NUMBER BOTH SIDES CAN REACH INDEPENDENTLY. That is the
5
+ * whole of what the protocol needs from a unit: everything else -- the offer, the reservation, the
6
+ * reconciliation, the covenant -- works on integers and does not care what was counted.
7
+ *
8
+ * This file used to be `tokenizer.ts`, and the rename is a finding rather than tidying. Writing a
9
+ * second unit showed that `tokenizer` was a unit-SPECIFIC field sitting in a unit-AGNOSTIC message:
10
+ * it made sense for tokens and meant nothing for bytes. A protocol that claims its unit is
11
+ * pluggable should not name one unit's machinery in every Offer.
12
+ *
13
+ * EXACT AND INEXACT METERS ARE DIFFERENT ANIMALS, and the difference decides the tolerance.
14
+ *
15
+ * A tokeniser is a LOSSY map from bytes to a count. Two correct implementations can agree on
16
+ * every byte and still disagree by one token, because the boundaries are a property of the
17
+ * tokeniser rather than of the content. Study A measured exactly that: three counter-examples
18
+ * in 3,634 adversarial trials, each one token.
19
+ *
20
+ * Counting octets is a DIRECT function of the bytes. Two parties who agree on `contentDigest`
21
+ * cannot disagree on the length -- the digest already proves the bytes are identical. A
22
+ * tolerance would not absorb honest divergence, because there is none; it would only widen the
23
+ * room a counterparty can shave in.
24
+ *
25
+ * So `exact` is not a performance note. It is what makes `toleranceAbs` of 0 correct for one unit
26
+ * and wrong for another, and §3.1's floor comes from the meter rather than from the protocol.
27
+ */
28
+ import { encode as encodeO200k } from 'gpt-tokenizer/encoding/o200k_base';
29
+ export class MeterUnavailable extends Error {
30
+ }
31
+ /**
32
+ * `o200k_base` is GPT-4o's tokeniser, and the one every measured number in SPEC.md was taken with:
33
+ * Study A's symmetry results, Study C's residual baseline, and the provider agreement study.
34
+ */
35
+ const o200k = {
36
+ name: 'o200k_base',
37
+ unit: 'llm.output_tokens.v1',
38
+ // A tokeniser measures TEXT, so this meter decodes first -- which is the honest statement of
39
+ // what it is: a text meter, usable only on content that is text. The decode is deliberately
40
+ // non-fatal, because a tokeniser's job is to count what it was given rather than to validate
41
+ // it, and a halt on malformed input belongs to the digest check in SPEC.md 5 rule 3.
42
+ count: (content) => encodeO200k(new TextDecoder().decode(content)).length,
43
+ exact: false,
44
+ };
45
+ /**
46
+ * Octets of the delivered content, UTF-8, exactly as `contentDigest` covers them.
47
+ *
48
+ * The strongest meter in the protocol, and the simplest: the parties already agree on the bytes by
49
+ * the time counting matters, because §5 rule 3 halts on a digest mismatch before any tolerance is
50
+ * consulted. Once the digests match, the lengths are the same number or one side is broken.
51
+ */
52
+ const octets = {
53
+ name: 'octets',
54
+ unit: 'net.bytes_delivered.v1',
55
+ // Now that delivery is bytes, this is the whole meter. It used to encode a string to UTF-8
56
+ // first, which measured text correctly and could not measure a file at all.
57
+ count: (content) => content.length,
58
+ exact: true,
59
+ };
60
+ const REGISTRY = { o200k_base: o200k, octets };
61
+ /**
62
+ * The meter an Offer names, or throw.
63
+ *
64
+ * NO FALLBACK, DELIBERATELY. Counting with a meter other than the one agreed produces a number
65
+ * that looks like a measurement, settles like one, and is not one. §3.1 requires an Offer whose
66
+ * meter cannot be obtained to be refused, and this is where that refusal happens.
67
+ */
68
+ export function resolveMeter(name, unit) {
69
+ const meter = REGISTRY[name];
70
+ if (!meter) {
71
+ throw new MeterUnavailable(`cannot obtain meter "${name}" -- SPEC.md 3.1 requires refusing an Offer whose meter is ` +
72
+ `unresolvable, rather than counting with a different one. available: ${Object.keys(REGISTRY).join(', ')}`);
73
+ }
74
+ if (unit !== undefined && meter.unit !== unit) {
75
+ throw new MeterUnavailable(`meter "${name}" measures ${meter.unit}, but the Offer asks for ${unit}`);
76
+ }
77
+ return meter;
78
+ }
79
+ /**
80
+ * The smallest tolerance an Offer may carry for this meter.
81
+ *
82
+ * SPEC.md §3.1. An exact meter permits 0, and should use it: honest divergence is impossible, so
83
+ * any tolerance is pure shaving room. An inexact meter requires at least 1, because Study A
84
+ * measured a one-token disagreement between honest parties and a zero tolerance would halt them.
85
+ */
86
+ export const minimumTolerance = (meter) => (meter.exact ? 0 : 1);
87
+ /** A counting function for the named meter, for callers that want only the number. */
88
+ export function meterFor(name, unit) {
89
+ const meter = resolveMeter(name, unit);
90
+ return (content) => meter.count(content);
91
+ }
92
+ /** Token ids, for the conformance comparison that pins boundaries rather than totals. */
93
+ export function encodeWith(name, text) {
94
+ if (name !== 'o200k_base')
95
+ throw new MeterUnavailable(`${name} does not produce token ids`);
96
+ return encodeO200k(text);
97
+ }
98
+ /** Every meter this implementation can serve, for a provider building an Offer. */
99
+ export const available = () => Object.keys(REGISTRY);
100
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","sourceRoot":"","sources":["../../src/meter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAE1E,MAAM,OAAO,gBAAiB,SAAQ,KAAK;CAAG;AAiB9C;;;GAGG;AACH,MAAM,KAAK,GAAU;IACnB,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,sBAAsB;IAC5B,6FAA6F;IAC7F,4FAA4F;IAC5F,6FAA6F;IAC7F,qFAAqF;IACrF,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;IACzE,KAAK,EAAE,KAAK;CACb,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,GAAU;IACpB,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,wBAAwB;IAC9B,2FAA2F;IAC3F,4EAA4E;IAC5E,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM;IAClC,KAAK,EAAE,IAAI;CACZ,CAAC;AAEF,MAAM,QAAQ,GAA0B,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,IAAa;IACtD,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,gBAAgB,CACxB,wBAAwB,IAAI,6DAA6D;YACvF,uEAAuE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC5G,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAC9C,MAAM,IAAI,gBAAgB,CACxB,UAAU,IAAI,cAAc,KAAK,CAAC,IAAI,4BAA4B,IAAI,EAAE,CACzE,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAY,EAAU,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEhF,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,IAAa;IAClD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACvC,OAAO,CAAC,OAAmB,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACvD,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAY;IACnD,IAAI,IAAI,KAAK,YAAY;QAAE,MAAM,IAAI,gBAAgB,CAAC,GAAG,IAAI,6BAA6B,CAAC,CAAC;IAC5F,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,SAAS,GAAG,GAAa,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { SessionHistory } from './history.js';
2
+ import type { Offer } from './types.js';
3
+ export declare class OfferRejected extends Error {
4
+ }
5
+ export declare function acceptOffer(offer: Offer, expectedNetwork?: string, history?: SessionHistory): Offer;
6
+ //# sourceMappingURL=offer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offer.d.ts","sourceRoot":"","sources":["../../src/offer.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAQxC,qBAAa,aAAc,SAAQ,KAAK;CAAG;AA2D3C,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,KAAK,CAUnG"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * SPEC.md §3.1 -- what a buyer MUST reject before spending anything.
3
+ *
4
+ * The Offer is the provider's signed commitment and every later message is bound to it, so this
5
+ * is the only moment a buyer can decline cheaply. After it accepts, the exposure bound, the
6
+ * tolerance and the response window are all fixed by terms it agreed to.
7
+ *
8
+ * Three of these rejections are named in §3.1 and the rest come from field constraints in the same
9
+ * table. They are enforced together because a buyer that checks some of them is not safer in any
10
+ * useful sense -- an Offer with `babelUnits` of zero is as unusable as one with no tokeniser.
11
+ */
12
+ import { verify } from './encoding.js';
13
+ import { resolveMeter, minimumTolerance } from './meter.js';
14
+ /** §7.3: the window lowers to OpCheckSequenceVerify, whose low 32 bits carry the delay. */
15
+ const MAX_RESPONSE_WINDOW = 4294967295;
16
+ /** SPEC.md 3.1: 16 bytes. 128 bits of nonce -- see 3.1a for why more would not help. */
17
+ const SESSION_ID = /^[0-9a-f]{32}$/;
18
+ export class OfferRejected extends Error {
19
+ }
20
+ const reject = (why) => {
21
+ throw new OfferRejected(why);
22
+ };
23
+ /** The numeric floors from the §3.1 table, each of which makes the Offer unusable if breached. */
24
+ function checkBounds(offer) {
25
+ if (offer.unitPriceSompi < 1)
26
+ reject('unitPriceSompi must be >= 1');
27
+ if (offer.babelUnits < 1)
28
+ reject('babelUnits must be >= 1');
29
+ if (offer.maxBabels < 1)
30
+ reject('maxBabels must be >= 1');
31
+ // THE FLOOR COMES FROM THE METER, NOT FROM THE PROTOCOL, and that only became visible when a
32
+ // second unit was added. A tokeniser is a lossy map from bytes to a count, so two honest parties
33
+ // can differ by one and a zero tolerance halts them -- Study A measured exactly that. Counting
34
+ // octets is a direct function of the bytes, so once §5 rule 3 has agreed the digest there is no
35
+ // honest divergence left to absorb, and a tolerance would be nothing but shaving room.
36
+ const floor = minimumTolerance(resolveMeter(offer.meter, offer.unit));
37
+ if (offer.toleranceAbs < floor) {
38
+ reject(`toleranceAbs must be >= ${floor} for meter ${offer.meter} -- see SPEC.md 6`);
39
+ }
40
+ if (offer.checkpointEvery < 0)
41
+ reject('checkpointEvery must be >= 0');
42
+ }
43
+ /**
44
+ * Accept an Offer, or throw. Returns the Offer so a caller can use it in an expression.
45
+ *
46
+ * `meter` is checked for presence only. Whether a given name RESOLVES is a property of the
47
+ * buyer's environment rather than of the message -- a buyer that cannot obtain it MUST refuse,
48
+ * but that refusal belongs where the meter is loaded (src/meter.ts), not here.
49
+ */
50
+ /** The fields that must simply be what §3.1 says, before any of them are worth interpreting. */
51
+ function checkShape(offer, expectedNetwork) {
52
+ if (offer.v !== 1)
53
+ reject(`unsupported version ${offer.v}`);
54
+ if (offer.scheme !== 'metered')
55
+ reject(`scheme is ${offer.scheme}, not metered`);
56
+ if (expectedNetwork && offer.network !== expectedNetwork) {
57
+ reject(`network is ${offer.network}, expected ${expectedNetwork}`);
58
+ }
59
+ if (!SESSION_ID.test(offer.sessionId))
60
+ reject('sessionId must be 16 hex bytes');
61
+ if (!offer.meter)
62
+ reject('meter is absent -- an unnamed meter is not usable');
63
+ if (offer.responseWindowDaa < 1 || offer.responseWindowDaa > MAX_RESPONSE_WINDOW) {
64
+ reject(`responseWindowDaa ${offer.responseWindowDaa} outside 1..=${MAX_RESPONSE_WINDOW}`);
65
+ }
66
+ }
67
+ /**
68
+ * SPEC.md §3.1a. `history.ts` says why the width of `sessionId` is not what does this job.
69
+ *
70
+ * Called LAST, after the signature has verified, so a malformed or unsigned Offer cannot burn an
71
+ * identifier the provider might go on to use honestly: an attacker who can hand the buyer junk
72
+ * must not be able to poison its history against the real provider.
73
+ */
74
+ function checkNovelty(offer, history) {
75
+ if (!history)
76
+ return;
77
+ if (history.seen(offer.providerPubkey, offer.sessionId)) {
78
+ reject(`sessionId ${offer.sessionId} has been offered by this provider before -- see SPEC.md 3.1a`);
79
+ }
80
+ history.record(offer.providerPubkey, offer.sessionId);
81
+ }
82
+ export function acceptOffer(offer, expectedNetwork, history) {
83
+ checkShape(offer, expectedNetwork);
84
+ checkBounds(offer);
85
+ // Before novelty and after everything cheap: it is the most expensive check, and it is what
86
+ // makes `providerPubkey` mean anything, which the novelty history is keyed by.
87
+ if (!verify(offer, offer.providerPubkey))
88
+ reject('Offer signature does not verify');
89
+ checkNovelty(offer, history);
90
+ return offer;
91
+ }
92
+ //# sourceMappingURL=offer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offer.js","sourceRoot":"","sources":["../../src/offer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Halt, Measurement, Offer, Reconciled } from './types.js';
2
+ /**
3
+ * SPEC.md §5 rule 4. The bound is ABSOLUTE, and does not scale with the size of the babel.
4
+ *
5
+ * `toleranceRel` was removed in this version because the evidence never supported it. Honest
6
+ * divergence between two correct implementations is a BOUNDARY effect: Study A found it three
7
+ * times in 3,634 adversarial trials, and every occurrence was exactly one token. Study C looked
8
+ * for it at seven babel sizes from 3 to 550 units, 4,000 boundaries each, and found none at any
9
+ * size -- so the magnitude does not grow with the babel, and a bound that grows with the babel is
10
+ * answering a problem nobody has measured.
11
+ *
12
+ * What such a bound WOULD do is widen the room a counterparty can shave in, which is the exact
13
+ * leak §5.1 exists to bound: at 0.2% of a 5,000-unit babel it is ten free tokens per babel,
14
+ * justified by nothing.
15
+ */
16
+ export declare function toleranceBound(offer: Offer, providerUnits: number): number;
17
+ export declare function reconcileBabel(offer: Offer, buyer: Measurement, provider: Measurement, seq: number): Reconciled | Halt;
18
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/reconcile.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIvE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAG1E;AAwDD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,WAAW,EACrB,GAAG,EAAE,MAAM,GACV,UAAU,GAAG,IAAI,CAkBnB"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * SPEC.md §5 -- the reconciliation rule, in order, halting on the first failure.
3
+ *
4
+ * The order is normative and it is not arbitrary. Signatures come first because an unsigned
5
+ * Measurement is not evidence of anything; the content digest comes before the counts because if
6
+ * the two parties are describing different bytes then no amount of counting can reconcile them,
7
+ * and a tolerance comparison on mismatched content would silently succeed whenever the numbers
8
+ * happened to land close together.
9
+ *
10
+ * Rule 6 bills the LOWER count. That removes the provider's incentive to sit at the top of the
11
+ * tolerance band -- and hands the identical trick to the buyer, which is why §5.1's bias detector
12
+ * is normative and symmetric. See bias.ts.
13
+ */
14
+ import { verify } from './encoding.js';
15
+ const halt = (reason, detail) => ({ ok: false, reason, detail });
16
+ /**
17
+ * SPEC.md §5 rule 4. The bound is ABSOLUTE, and does not scale with the size of the babel.
18
+ *
19
+ * `toleranceRel` was removed in this version because the evidence never supported it. Honest
20
+ * divergence between two correct implementations is a BOUNDARY effect: Study A found it three
21
+ * times in 3,634 adversarial trials, and every occurrence was exactly one token. Study C looked
22
+ * for it at seven babel sizes from 3 to 550 units, 4,000 boundaries each, and found none at any
23
+ * size -- so the magnitude does not grow with the babel, and a bound that grows with the babel is
24
+ * answering a problem nobody has measured.
25
+ *
26
+ * What such a bound WOULD do is widen the room a counterparty can shave in, which is the exact
27
+ * leak §5.1 exists to bound: at 0.2% of a 5,000-unit babel it is ten free tokens per babel,
28
+ * justified by nothing.
29
+ */
30
+ export function toleranceBound(offer, providerUnits) {
31
+ void providerUnits;
32
+ return offer.toleranceAbs;
33
+ }
34
+ /**
35
+ * Rule 1. Each Measurement must verify against the key for the party it claims to be from.
36
+ *
37
+ * `by` is checked against the key that actually signed, not merely read -- otherwise a provider
38
+ * could sign a Measurement stamped `by: "buyer"` and supply both halves of the reconciliation.
39
+ */
40
+ function signedByClaimedParty(m, offer) {
41
+ const key = m.by === 'buyer' ? offer.buyerPubkey : offer.providerPubkey;
42
+ return verify(m, key);
43
+ }
44
+ /**
45
+ * Reconcile one babel boundary. Returns what to bill, or the reason to stop.
46
+ *
47
+ * Idempotency (§3.3, `measurementId`) is the caller's job: a retransmitted Measurement MUST be a
48
+ * no-op, and that belongs to whatever holds session state, not to a pure rule.
49
+ */
50
+ /** Rule 1. Both signatures verify, and each `by` matches the key that actually signed. */
51
+ function checkAuthorship(offer, buyer, provider) {
52
+ if (buyer.by !== 'buyer' || provider.by !== 'provider') {
53
+ return halt('signature', `measurements are ${buyer.by}/${provider.by}, expected buyer/provider`);
54
+ }
55
+ if (!signedByClaimedParty(buyer, offer))
56
+ return halt('signature', 'buyer measurement does not verify');
57
+ if (!signedByClaimedParty(provider, offer))
58
+ return halt('signature', 'provider measurement does not verify');
59
+ return null;
60
+ }
61
+ /** Rule 2, plus X1: this session, this babel, both sides. */
62
+ function checkPlacement(offer, buyer, provider, seq) {
63
+ if (buyer.sessionId !== offer.sessionId || provider.sessionId !== offer.sessionId) {
64
+ return halt('sequence', 'measurement sessionId does not match the Offer');
65
+ }
66
+ if (buyer.seq !== seq || provider.seq !== seq) {
67
+ return halt('sequence', `expected seq ${seq}, got buyer ${buyer.seq} / provider ${provider.seq}`);
68
+ }
69
+ return null;
70
+ }
71
+ /** Rules 4 and 5. Reached only once the parties are known to be describing the same bytes. */
72
+ function checkTolerances(offer, buyer, provider) {
73
+ const bound = toleranceBound(offer, provider.units);
74
+ if (Math.abs(buyer.units - provider.units) > bound) {
75
+ return halt('tolerance-babel', `|${buyer.units} − ${provider.units}| exceeds ${bound}`);
76
+ }
77
+ const cumulativeBound = toleranceBound(offer, provider.cumulativeUnits);
78
+ if (Math.abs(buyer.cumulativeUnits - provider.cumulativeUnits) > cumulativeBound) {
79
+ return halt('tolerance-cumulative', `|${buyer.cumulativeUnits} − ${provider.cumulativeUnits}| exceeds ${cumulativeBound}`);
80
+ }
81
+ return null;
82
+ }
83
+ export function reconcileBabel(offer, buyer, provider, seq) {
84
+ // Rule 3 sits between the groups rather than inside one, because its ORDER is the point: it runs
85
+ // before any counting, so mismatched bytes can never pass on numbers that happen to land close.
86
+ const failure = checkAuthorship(offer, buyer, provider) ??
87
+ checkPlacement(offer, buyer, provider, seq) ??
88
+ (buyer.contentDigest !== provider.contentDigest
89
+ ? halt('content-digest', 'the parties are describing different bytes')
90
+ : null) ??
91
+ checkTolerances(offer, buyer, provider);
92
+ if (failure)
93
+ return failure;
94
+ return {
95
+ ok: true,
96
+ billedUnits: Math.min(buyer.units, provider.units),
97
+ billedCumulativeUnits: Math.min(buyer.cumulativeUnits, provider.cumulativeUnits),
98
+ residual: provider.units - buyer.units,
99
+ };
100
+ }
101
+ //# sourceMappingURL=reconcile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/reconcile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { Offer, Reservation } from './types.js';
2
+ export declare class ReservationRejected extends Error {
3
+ }
4
+ /**
5
+ * The Reservation did not come from the buyer named in the Offer -- unsigned, wrongly signed, or
6
+ * altered after signing.
7
+ *
8
+ * A SUBTYPE, because the difference matters to the caller and to nobody else. Every other
9
+ * ReservationRejected means an AUTHENTICATED buyer asked for something it had agreed not to ask
10
+ * for, which is a disagreement and stops the session. This one means the sender is not the buyer
11
+ * at all, so it is evidence about the sender and none about the session -- and since `sessionId`
12
+ * travels in clear, treating it as a disagreement would let any observer kill any session.
13
+ */
14
+ export declare class ReservationUnauthenticated extends ReservationRejected {
15
+ }
16
+ /** What the previous chunk left behind. `null` means nothing has been reserved yet. */
17
+ export interface BabelCursor {
18
+ seq: number;
19
+ cumulativeUnits: number;
20
+ cumulativeSompi: number;
21
+ stateDigest: string | null;
22
+ }
23
+ /** The amount a chunk costs, recomputed from the Offer's price rather than read off the wire. */
24
+ export declare const priceOf: (offer: Offer, units: number) => number;
25
+ /**
26
+ * The funding floor: the most a session can ever bill, plus the fee its close will cost.
27
+ *
28
+ * SPEC.md 7.4b. `expire` pays the provider EXACTLY `pendingSompi`, so if the parties agree a total
29
+ * the covenant cannot pay -- including the network fee for the transaction that pays it -- then no
30
+ * valid close transaction exists and the whole balance is stranded. The covenant cannot catch this
31
+ * itself: it would have to know the fee a future transaction will cost, which is not available to
32
+ * a script. The buyer can, because the buyer chooses the funding amount, and it can do so before
33
+ * spending anything.
34
+ */
35
+ export declare function requiredFunding(offer: Offer, closeFeeSompi?: number): number;
36
+ /**
37
+ * The smallest balance a covenant may hold, and it is not a dust rule -- it is a SHAPE rule.
38
+ *
39
+ * A two-output close needs both halves to clear KIP-9 together, and below roughly 0.068 KAS they
40
+ * cannot: tools/dust-map.ts finds claims with no legal close at all for balances between
41
+ * 5,700,000 and 6,800,000 sompi, whatever the dust constant is set to. Above 6,850,000 the
42
+ * problem disappears. 10,000,000 is the round number above that with margin.
43
+ */
44
+ export declare const MIN_COVENANT_SOMPI = 10000000;
45
+ /**
46
+ * The covenant's fee allowance, measured: `expire` performs two signature checks
47
+ * and is the costliest entry, needing ~313,800 -- 400,000 is what the contract reserves.
48
+ */
49
+ export declare const CLOSE_FEE_SOMPI = 400000;
50
+ export declare function acceptReservation(offer: Offer, reservation: Reservation, previous: BabelCursor | null): BabelCursor;
51
+ //# sourceMappingURL=reservation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reservation.d.ts","sourceRoot":"","sources":["../../src/reservation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAErD,qBAAa,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;;;;;;;GASG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;CAAG;AAMtE,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,iGAAiG;AACjG,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,EAAE,OAAO,MAAM,KAAG,MAAsC,CAAC;AA8C7F;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,aAAa,SAAkB,GAAG,MAAM,CAGrF;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,WAAa,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,eAAe,SAAU,CAAC;AAEvC,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,WAAW,GAAG,IAAI,GAC3B,WAAW,CA4Bb"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * SPEC.md §3.2 -- the Reservation. One babel, never the session.
3
+ *
4
+ * This is where the exposure bound is actually enforced. The buyer signs an authorisation for a
5
+ * single babel, and the mirror threat -- "take a babel, never sign its State" -- is the
6
+ * provider's matching exposure. Both are one chunk, deliberately and symmetrically: `babelUnits`
7
+ * is the whole trust model, which is why the checks here are not conveniences.
8
+ *
9
+ * "Recomputed and compared, never trusted" (§3.2) is the load-bearing sentence. A provider that
10
+ * reads `cumulativeSompi` off the wire and bills it has handed the buyer a free variable.
11
+ */
12
+ import { verify } from './encoding.js';
13
+ export class ReservationRejected extends Error {
14
+ }
15
+ /**
16
+ * The Reservation did not come from the buyer named in the Offer -- unsigned, wrongly signed, or
17
+ * altered after signing.
18
+ *
19
+ * A SUBTYPE, because the difference matters to the caller and to nobody else. Every other
20
+ * ReservationRejected means an AUTHENTICATED buyer asked for something it had agreed not to ask
21
+ * for, which is a disagreement and stops the session. This one means the sender is not the buyer
22
+ * at all, so it is evidence about the sender and none about the session -- and since `sessionId`
23
+ * travels in clear, treating it as a disagreement would let any observer kill any session.
24
+ */
25
+ export class ReservationUnauthenticated extends ReservationRejected {
26
+ }
27
+ const refuse = (why) => {
28
+ throw new ReservationRejected(why);
29
+ };
30
+ /** The amount a chunk costs, recomputed from the Offer's price rather than read off the wire. */
31
+ export const priceOf = (offer, units) => units * offer.unitPriceSompi;
32
+ /**
33
+ * Check a Reservation against the Offer and the session so far. Throws, or returns the cursor the
34
+ * next chunk must build on.
35
+ */
36
+ /**
37
+ * §3.2: "from 0, strictly incrementing by exactly 1". Exactly one, so B4 -- replaying an old
38
+ * Reservation -- cannot advance the session, and no chunk can be skipped past unbilled.
39
+ */
40
+ function checkSequencing(offer, reservation, previous) {
41
+ if (reservation.v !== 1)
42
+ refuse(`unsupported version ${reservation.v}`);
43
+ if (reservation.sessionId !== offer.sessionId)
44
+ refuse('sessionId does not match the Offer');
45
+ const expectedSeq = previous === null ? 0 : previous.seq + 1;
46
+ if (reservation.seq !== expectedSeq)
47
+ refuse(`seq must be exactly ${expectedSeq}, got ${reservation.seq}`);
48
+ if (reservation.seq >= offer.maxBabels) {
49
+ refuse(`seq ${reservation.seq} reaches the session ceiling of ${offer.maxBabels} chunks`);
50
+ }
51
+ }
52
+ /** THE EXPOSURE BOUND. Everything else in this protocol is bookkeeping around these two lines. */
53
+ function checkExposure(offer, reservation) {
54
+ if (reservation.units < 1)
55
+ refuse('units must be >= 1');
56
+ if (reservation.units > offer.babelUnits) {
57
+ refuse(`units ${reservation.units} exceeds babelUnits ${offer.babelUnits}`);
58
+ }
59
+ }
60
+ /** §3.2: recomputed and compared, never trusted. Returns the totals the caller should keep. */
61
+ function recomputeTotals(offer, reservation, previous) {
62
+ const units = (previous?.cumulativeUnits ?? 0) + reservation.units;
63
+ const sompi = (previous?.cumulativeSompi ?? 0) + priceOf(offer, reservation.units);
64
+ if (reservation.cumulativeUnits !== units) {
65
+ refuse(`cumulativeUnits is ${reservation.cumulativeUnits}, recomputed ${units}`);
66
+ }
67
+ if (reservation.cumulativeSompi !== sompi) {
68
+ refuse(`cumulativeSompi is ${reservation.cumulativeSompi}, recomputed ${sompi}`);
69
+ }
70
+ return { units, sompi };
71
+ }
72
+ /**
73
+ * The funding floor: the most a session can ever bill, plus the fee its close will cost.
74
+ *
75
+ * SPEC.md 7.4b. `expire` pays the provider EXACTLY `pendingSompi`, so if the parties agree a total
76
+ * the covenant cannot pay -- including the network fee for the transaction that pays it -- then no
77
+ * valid close transaction exists and the whole balance is stranded. The covenant cannot catch this
78
+ * itself: it would have to know the fee a future transaction will cost, which is not available to
79
+ * a script. The buyer can, because the buyer chooses the funding amount, and it can do so before
80
+ * spending anything.
81
+ */
82
+ export function requiredFunding(offer, closeFeeSompi = CLOSE_FEE_SOMPI) {
83
+ const maxBill = offer.maxBabels * offer.babelUnits * offer.unitPriceSompi;
84
+ return Math.max(maxBill + closeFeeSompi, MIN_COVENANT_SOMPI);
85
+ }
86
+ /**
87
+ * The smallest balance a covenant may hold, and it is not a dust rule -- it is a SHAPE rule.
88
+ *
89
+ * A two-output close needs both halves to clear KIP-9 together, and below roughly 0.068 KAS they
90
+ * cannot: tools/dust-map.ts finds claims with no legal close at all for balances between
91
+ * 5,700,000 and 6,800,000 sompi, whatever the dust constant is set to. Above 6,850,000 the
92
+ * problem disappears. 10,000,000 is the round number above that with margin.
93
+ */
94
+ export const MIN_COVENANT_SOMPI = 10_000_000;
95
+ /**
96
+ * The covenant's fee allowance, measured: `expire` performs two signature checks
97
+ * and is the costliest entry, needing ~313,800 -- 400,000 is what the contract reserves.
98
+ */
99
+ export const CLOSE_FEE_SOMPI = 400_000;
100
+ export function acceptReservation(offer, reservation, previous) {
101
+ // SPEC.md 3.2: "Signed by the buyer." Until 2026-09-10 nothing checked that, so the provider
102
+ // would deliver work to anyone who knew a sessionId, and held no evidence the buyer had ever
103
+ // authorised the babel it was about to bill for. FIRST, because every check below it is
104
+ // interpreting a document whose author is otherwise unknown.
105
+ if (!verify(reservation, offer.buyerPubkey)) {
106
+ throw new ReservationUnauthenticated('Reservation signature does not verify against the buyer named in the Offer');
107
+ }
108
+ checkSequencing(offer, reservation, previous);
109
+ checkExposure(offer, reservation);
110
+ const totals = recomputeTotals(offer, reservation, previous);
111
+ // §3.2: the digest of the previous DOUBLY-SIGNED State, null at seq 0. This is what stops a
112
+ // Reservation being lifted out of one history and dropped into another.
113
+ const expectedPrev = previous?.stateDigest ?? null;
114
+ if (reservation.prevState !== expectedPrev) {
115
+ refuse(`prevState ${reservation.prevState ?? 'null'} does not chain to ${expectedPrev ?? 'null'}`);
116
+ }
117
+ return {
118
+ seq: reservation.seq,
119
+ cumulativeUnits: totals.units,
120
+ cumulativeSompi: totals.sompi,
121
+ stateDigest: null, // filled in once the State for this chunk is doubly signed
122
+ };
123
+ }
124
+ //# sourceMappingURL=reservation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reservation.js","sourceRoot":"","sources":["../../src/reservation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { type BiasState } from './bias.js';
2
+ import { type SignerStore } from './signer.js';
3
+ import { Checkpointer } from './checkpoint.js';
4
+ import type { Halt, Measurement, Offer, Reservation, State } from './types.js';
5
+ export interface ChunkResult {
6
+ state: State;
7
+ buyerSig: string;
8
+ providerSig: string;
9
+ billedUnits: number;
10
+ checkpointed: boolean;
11
+ }
12
+ export declare class SessionHalted extends Error {
13
+ readonly halt: Halt;
14
+ constructor(halt: Halt);
15
+ }
16
+ /**
17
+ * Drives one session for BOTH parties, which is what a test needs and what a real deployment
18
+ * splits in half. Keeping them together here is the only way to assert that the two halves agree.
19
+ */
20
+ export declare class Session {
21
+ private readonly offer;
22
+ private readonly buyerStore;
23
+ private readonly providerStore;
24
+ private readonly checkpointer;
25
+ private cursor;
26
+ private bias;
27
+ constructor(offer: Offer, buyerStore: SignerStore, providerStore: SignerStore, checkpointer: Checkpointer);
28
+ get residualState(): BiasState;
29
+ get cumulativeSompi(): number;
30
+ /**
31
+ * Run one chunk end to end: reserve, reconcile, bill, doubly sign, checkpoint.
32
+ *
33
+ * Throws SessionHalted on any rule failure. §1: "The only remedy for disagreement is to stop."
34
+ */
35
+ chunk(reservation: Reservation, buyerMeasurement: Measurement, providerMeasurement: Measurement, keys: {
36
+ buyerSk: string;
37
+ providerSk: string;
38
+ }): ChunkResult;
39
+ }
40
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/session.ts"],"names":[],"mappings":"AAiBA,OAAO,EAA4C,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACrF,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE/E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,qBAAa,aAAc,SAAQ,KAAK;IAC1B,QAAQ,CAAC,IAAI,EAAE,IAAI;gBAAV,IAAI,EAAE,IAAI;CAGhC;AAED;;;GAGG;AACH,qBAAa,OAAO;IAKhB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAP/B,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAA6B;gBAGtB,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,WAAW,EACvB,aAAa,EAAE,WAAW,EAC1B,YAAY,EAAE,YAAY;IAG7C,IAAI,aAAa,IAAI,SAAS,CAE7B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED;;;;OAIG;IACH,KAAK,CACH,WAAW,EAAE,WAAW,EACxB,gBAAgB,EAAE,WAAW,EAC7B,mBAAmB,EAAE,WAAW,EAChC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC5C,WAAW;CA0Cf"}