@tabai/sdk 0.2.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 (204) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +401 -0
  3. package/bin/tab.mjs +23 -0
  4. package/dist/_shared/abi.d.ts +150 -0
  5. package/dist/_shared/abi.d.ts.map +1 -0
  6. package/dist/_shared/abi.js +197 -0
  7. package/dist/_shared/abi.js.map +1 -0
  8. package/dist/_shared/chains.d.ts +118 -0
  9. package/dist/_shared/chains.d.ts.map +1 -0
  10. package/dist/_shared/chains.js +89 -0
  11. package/dist/_shared/chains.js.map +1 -0
  12. package/dist/_shared/hex.d.ts +35 -0
  13. package/dist/_shared/hex.d.ts.map +1 -0
  14. package/dist/_shared/hex.js +40 -0
  15. package/dist/_shared/hex.js.map +1 -0
  16. package/dist/_shared/index.d.ts +14 -0
  17. package/dist/_shared/index.d.ts.map +1 -0
  18. package/dist/_shared/index.js +14 -0
  19. package/dist/_shared/index.js.map +1 -0
  20. package/dist/_shared/keccak256.d.ts +29 -0
  21. package/dist/_shared/keccak256.d.ts.map +1 -0
  22. package/dist/_shared/keccak256.js +145 -0
  23. package/dist/_shared/keccak256.js.map +1 -0
  24. package/dist/_shared/result.d.ts +78 -0
  25. package/dist/_shared/result.d.ts.map +1 -0
  26. package/dist/_shared/result.js +61 -0
  27. package/dist/_shared/result.js.map +1 -0
  28. package/dist/cli/client-config.d.ts +155 -0
  29. package/dist/cli/client-config.d.ts.map +1 -0
  30. package/dist/cli/client-config.js +382 -0
  31. package/dist/cli/client-config.js.map +1 -0
  32. package/dist/cli/connect.d.ts +76 -0
  33. package/dist/cli/connect.d.ts.map +1 -0
  34. package/dist/cli/connect.js +158 -0
  35. package/dist/cli/connect.js.map +1 -0
  36. package/dist/cli/doctor.d.ts +57 -0
  37. package/dist/cli/doctor.d.ts.map +1 -0
  38. package/dist/cli/doctor.js +253 -0
  39. package/dist/cli/doctor.js.map +1 -0
  40. package/dist/cli/index.d.ts +13 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +13 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/cli/main.d.ts +45 -0
  45. package/dist/cli/main.d.ts.map +1 -0
  46. package/dist/cli/main.js +371 -0
  47. package/dist/cli/main.js.map +1 -0
  48. package/dist/errors.d.ts +29 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +37 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/http/client-402.d.ts +243 -0
  53. package/dist/http/client-402.d.ts.map +1 -0
  54. package/dist/http/client-402.js +515 -0
  55. package/dist/http/client-402.js.map +1 -0
  56. package/dist/http/headers.d.ts +173 -0
  57. package/dist/http/headers.d.ts.map +1 -0
  58. package/dist/http/headers.js +284 -0
  59. package/dist/http/headers.js.map +1 -0
  60. package/dist/http/index.d.ts +15 -0
  61. package/dist/http/index.d.ts.map +1 -0
  62. package/dist/http/index.js +15 -0
  63. package/dist/http/index.js.map +1 -0
  64. package/dist/http/metering-claim.d.ts +82 -0
  65. package/dist/http/metering-claim.d.ts.map +1 -0
  66. package/dist/http/metering-claim.js +99 -0
  67. package/dist/http/metering-claim.js.map +1 -0
  68. package/dist/index.d.ts +48 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +51 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/logger.d.ts +40 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +50 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/mcp/assets.d.ts +31 -0
  77. package/dist/mcp/assets.d.ts.map +1 -0
  78. package/dist/mcp/assets.js +78 -0
  79. package/dist/mcp/assets.js.map +1 -0
  80. package/dist/mcp/index.d.ts +19 -0
  81. package/dist/mcp/index.d.ts.map +1 -0
  82. package/dist/mcp/index.js +19 -0
  83. package/dist/mcp/index.js.map +1 -0
  84. package/dist/mcp/json-schema.d.ts +86 -0
  85. package/dist/mcp/json-schema.d.ts.map +1 -0
  86. package/dist/mcp/json-schema.js +215 -0
  87. package/dist/mcp/json-schema.js.map +1 -0
  88. package/dist/mcp/json.d.ts +43 -0
  89. package/dist/mcp/json.d.ts.map +1 -0
  90. package/dist/mcp/json.js +69 -0
  91. package/dist/mcp/json.js.map +1 -0
  92. package/dist/mcp/registry-client.d.ts +88 -0
  93. package/dist/mcp/registry-client.d.ts.map +1 -0
  94. package/dist/mcp/registry-client.js +158 -0
  95. package/dist/mcp/registry-client.js.map +1 -0
  96. package/dist/mcp/schemas.d.ts +82 -0
  97. package/dist/mcp/schemas.d.ts.map +1 -0
  98. package/dist/mcp/schemas.js +493 -0
  99. package/dist/mcp/schemas.js.map +1 -0
  100. package/dist/mcp/server.d.ts +97 -0
  101. package/dist/mcp/server.d.ts.map +1 -0
  102. package/dist/mcp/server.js +285 -0
  103. package/dist/mcp/server.js.map +1 -0
  104. package/dist/mcp/settings.d.ts +90 -0
  105. package/dist/mcp/settings.d.ts.map +1 -0
  106. package/dist/mcp/settings.js +160 -0
  107. package/dist/mcp/settings.js.map +1 -0
  108. package/dist/mcp/toolset.d.ts +231 -0
  109. package/dist/mcp/toolset.d.ts.map +1 -0
  110. package/dist/mcp/toolset.js +760 -0
  111. package/dist/mcp/toolset.js.map +1 -0
  112. package/dist/payments/abi.d.ts +9 -0
  113. package/dist/payments/abi.d.ts.map +1 -0
  114. package/dist/payments/abi.js +17 -0
  115. package/dist/payments/abi.js.map +1 -0
  116. package/dist/payments/config.d.ts +199 -0
  117. package/dist/payments/config.d.ts.map +1 -0
  118. package/dist/payments/config.js +259 -0
  119. package/dist/payments/config.js.map +1 -0
  120. package/dist/payments/index.d.ts +13 -0
  121. package/dist/payments/index.d.ts.map +1 -0
  122. package/dist/payments/index.js +13 -0
  123. package/dist/payments/index.js.map +1 -0
  124. package/dist/payments/kuru.d.ts +191 -0
  125. package/dist/payments/kuru.d.ts.map +1 -0
  126. package/dist/payments/kuru.js +377 -0
  127. package/dist/payments/kuru.js.map +1 -0
  128. package/dist/payments/monad.d.ts +69 -0
  129. package/dist/payments/monad.d.ts.map +1 -0
  130. package/dist/payments/monad.js +306 -0
  131. package/dist/payments/monad.js.map +1 -0
  132. package/dist/payments/permit2.d.ts +118 -0
  133. package/dist/payments/permit2.d.ts.map +1 -0
  134. package/dist/payments/permit2.js +366 -0
  135. package/dist/payments/permit2.js.map +1 -0
  136. package/dist/payments/registry.d.ts +119 -0
  137. package/dist/payments/registry.d.ts.map +1 -0
  138. package/dist/payments/registry.js +199 -0
  139. package/dist/payments/registry.js.map +1 -0
  140. package/dist/payments/strategy.d.ts +80 -0
  141. package/dist/payments/strategy.d.ts.map +1 -0
  142. package/dist/payments/strategy.js +103 -0
  143. package/dist/payments/strategy.js.map +1 -0
  144. package/dist/proxy/hooks.d.ts +90 -0
  145. package/dist/proxy/hooks.d.ts.map +1 -0
  146. package/dist/proxy/hooks.js +35 -0
  147. package/dist/proxy/hooks.js.map +1 -0
  148. package/dist/proxy/index.d.ts +9 -0
  149. package/dist/proxy/index.d.ts.map +1 -0
  150. package/dist/proxy/index.js +9 -0
  151. package/dist/proxy/index.js.map +1 -0
  152. package/dist/proxy/proxy.d.ts +156 -0
  153. package/dist/proxy/proxy.d.ts.map +1 -0
  154. package/dist/proxy/proxy.js +366 -0
  155. package/dist/proxy/proxy.js.map +1 -0
  156. package/dist/server/adapters/express.d.ts +89 -0
  157. package/dist/server/adapters/express.d.ts.map +1 -0
  158. package/dist/server/adapters/express.js +215 -0
  159. package/dist/server/adapters/express.js.map +1 -0
  160. package/dist/server/adapters/hono.d.ts +52 -0
  161. package/dist/server/adapters/hono.d.ts.map +1 -0
  162. package/dist/server/adapters/hono.js +61 -0
  163. package/dist/server/adapters/hono.js.map +1 -0
  164. package/dist/server/adapters/next.d.ts +52 -0
  165. package/dist/server/adapters/next.d.ts.map +1 -0
  166. package/dist/server/adapters/next.js +56 -0
  167. package/dist/server/adapters/next.js.map +1 -0
  168. package/dist/server/index.d.ts +30 -0
  169. package/dist/server/index.d.ts.map +1 -0
  170. package/dist/server/index.js +30 -0
  171. package/dist/server/index.js.map +1 -0
  172. package/dist/server/metering.d.ts +209 -0
  173. package/dist/server/metering.d.ts.map +1 -0
  174. package/dist/server/metering.js +365 -0
  175. package/dist/server/metering.js.map +1 -0
  176. package/dist/server/post-paid.d.ts +355 -0
  177. package/dist/server/post-paid.d.ts.map +1 -0
  178. package/dist/server/post-paid.js +512 -0
  179. package/dist/server/post-paid.js.map +1 -0
  180. package/dist/x402/client.d.ts +203 -0
  181. package/dist/x402/client.d.ts.map +1 -0
  182. package/dist/x402/client.js +337 -0
  183. package/dist/x402/client.js.map +1 -0
  184. package/dist/x402/hub.d.ts +79 -0
  185. package/dist/x402/hub.d.ts.map +1 -0
  186. package/dist/x402/hub.js +164 -0
  187. package/dist/x402/hub.js.map +1 -0
  188. package/dist/x402/index.d.ts +27 -0
  189. package/dist/x402/index.d.ts.map +1 -0
  190. package/dist/x402/index.js +27 -0
  191. package/dist/x402/index.js.map +1 -0
  192. package/dist/x402/proxy.d.ts +162 -0
  193. package/dist/x402/proxy.d.ts.map +1 -0
  194. package/dist/x402/proxy.js +198 -0
  195. package/dist/x402/proxy.js.map +1 -0
  196. package/dist/x402/server.d.ts +162 -0
  197. package/dist/x402/server.d.ts.map +1 -0
  198. package/dist/x402/server.js +306 -0
  199. package/dist/x402/server.js.map +1 -0
  200. package/dist/x402/wire.d.ts +104 -0
  201. package/dist/x402/wire.d.ts.map +1 -0
  202. package/dist/x402/wire.js +265 -0
  203. package/dist/x402/wire.js.map +1 -0
  204. package/package.json +61 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The Tab HTTP header contract, and the only parser and formatter for it.
3
+ *
4
+ * This module is the reconciliation point between the two halves of the wire
5
+ * protocol: the client wrapper in `client-402.ts` (R23.2) reads these headers off
6
+ * a Service response, and the server-side post-paid plugin writes them. Both
7
+ * halves go through the functions here rather than through their own string
8
+ * handling, so a disagreement about a format is a compile or test failure in one
9
+ * file instead of a silent mis-parse in production.
10
+ *
11
+ * ## The contract
12
+ *
13
+ * | Header | Direction | Required | Value format |
14
+ * | --- | --- | --- | --- |
15
+ * | `Tab-Charge-Amount` | response | yes, within the block | decimal integer Asset base units, no sign, no separators, no exponent |
16
+ * | `Tab-Charge-Asset` | response | yes, within the block | `<chainId>:<address>`: the decimal EVM chain id, a colon, then a `0x` 20-byte address |
17
+ * | `Tab-Charge-Service` | response | yes, within the block | `0x` 32-byte word: the `serviceId` |
18
+ * | `Tab-Charge-Tool` | response | yes, within the block | `0x` 32-byte word: the `bytes32` tool key from the applied price list |
19
+ * | `Tab-Open-Tab` | response | yes, within the block | decimal integer base units: the Open Tab for that Agent, Service, and Asset after this call |
20
+ * | `Tab-Headroom` | response | yes, within the block | decimal integer base units: headroom remaining for that Agent and Asset |
21
+ * | `Tab-Agent` | request | yes on a metered call | `0x` 20-byte address: the Agent's Monad address, its identity on the rail |
22
+ * | `Tab-Authorisation` | request | optional | `0x` 32-byte word: the `authKey` the Service should meter against |
23
+ *
24
+ * ## Five decisions the table does not show, each of which 15.3 must match
25
+ *
26
+ * **1. The six response headers are one all-or-nothing block.** A response
27
+ * carrying none of them is simply not a metered response, and
28
+ * {@link parseChargeHeaders} returns `ok(undefined)` for it, an unmetered health
29
+ * endpoint behind the same client must not become an error. A response carrying
30
+ * *some* of them is a malformed metered response and is an `err`, because the
31
+ * alternative is a client that silently records a charge against the wrong Asset
32
+ * or against no Asset at all. There is no partial block and no default value: a
33
+ * missing `Tab-Headroom` does not mean zero headroom.
34
+ *
35
+ * **2. Every amount is an integer count of Asset base units, parsed to
36
+ * `bigint`.** Never a `number`, never a decimal, never a rate. USDC is 6
37
+ * decimals, so `10_000` is one cent, and an Open Tab routinely exceeds the range
38
+ * a float represents exactly. `Number.parseInt` would round a large Open Tab into
39
+ * something that still looks plausible, which is worse than failing. The grammar
40
+ * is deliberately narrow, `/^[0-9]+$/`, so `1e6`, `1.0`, `1_000`, `+1`, `-1`,
41
+ * `0x10`, and `1,000` are all rejected rather than coerced.
42
+ *
43
+ * **3. `Tab-Charge-Asset` carries the chain id and the Asset address and nothing
44
+ * else.** Decimals and symbol are deliberately absent: they are registry facts,
45
+ * not per-call facts, and a Service that could restate an Asset's decimals on
46
+ * every response could restate what a base unit means. So the parsed
47
+ * {@link ChargedAsset} is a two-field reference rather than an `AssetRef`, and a
48
+ * caller that needs decimals resolves them from its own configuration.
49
+ *
50
+ * **4. The chain id is bounded as a `uint64` and not narrowed to Monad's two
51
+ * networks.** Refusing an unknown chain id here would close a set the
52
+ * payment-strategy seam deliberately leaves open (R23.6): a strategy settling on a
53
+ * network this release has never heard of should still be able to read its own
54
+ * charge headers.
55
+ *
56
+ * **5. The Asset address is lower-cased on parse, and `serviceId` and the tool
57
+ * key are not.** An address arrives checksummed from one source and lower-case
58
+ * from another, and two spellings of one Asset in a ledger keyed by Asset is a
59
+ * client that reports two Open Tabs where there is one. A `bytes32` word has no
60
+ * checksum convention to normalise, so it is carried through byte-for-byte.
61
+ *
62
+ * A `bytes32` tool key is usually a short name right-padded with zero bytes -
63
+ * `0x7461622e64656d6f…` is `tab.demo`, but nothing
64
+ * here decodes it. The key is what `ServiceRegistry.priceOf` is keyed on, and it
65
+ * is carried as the word it is so it round-trips into a contract call unchanged.
66
+ *
67
+ * Requirements: 23.2, 21.5
68
+ */
69
+ import { type Address, type Bytes32, type Result } from "../_shared/index.js";
70
+ /** Every Tab header name, in the casing this SDK writes. */
71
+ export declare const TAB_HEADER: {
72
+ readonly chargeAmount: "Tab-Charge-Amount";
73
+ readonly chargeAsset: "Tab-Charge-Asset";
74
+ readonly chargeService: "Tab-Charge-Service";
75
+ readonly chargeTool: "Tab-Charge-Tool";
76
+ readonly openTab: "Tab-Open-Tab";
77
+ readonly headroom: "Tab-Headroom";
78
+ readonly agent: "Tab-Agent";
79
+ readonly authorisation: "Tab-Authorisation";
80
+ };
81
+ /**
82
+ * The six response headers that form the charge block, all six required whenever
83
+ * any one of them is present.
84
+ */
85
+ export declare const CHARGE_RESPONSE_HEADERS: readonly ["Tab-Charge-Amount", "Tab-Charge-Asset", "Tab-Charge-Service", "Tab-Charge-Tool", "Tab-Open-Tab", "Tab-Headroom"];
86
+ /**
87
+ * The Asset a charge is denominated in, exactly as `Tab-Charge-Asset` carries it:
88
+ * a chain id and an address, with no decimals and no symbol.
89
+ */
90
+ export interface ChargedAsset {
91
+ /** The EVM chain id the Asset lives on: 143 for Monad Mainnet, 10143 for Monad Testnet. */
92
+ readonly chainId: bigint;
93
+ /** Lower-cased on parse, so one Asset has one spelling in a ledger keyed by it. */
94
+ readonly address: Address;
95
+ }
96
+ /** One decoded charge block: what the Service says this call cost and what it left. */
97
+ export interface ChargeBlock {
98
+ /** Integer Asset base units for this call. */
99
+ readonly amount: bigint;
100
+ readonly asset: ChargedAsset;
101
+ readonly serviceId: Bytes32;
102
+ /** The `bytes32` tool key from the applied price list. */
103
+ readonly tool: Bytes32;
104
+ /** The Open Tab for this Agent, Service, and Asset after this call. */
105
+ readonly openTab: bigint;
106
+ /** Headroom remaining for this Agent and Asset. */
107
+ readonly headroom: bigint;
108
+ }
109
+ /**
110
+ * Anything that answers a case-insensitive header lookup. A `Headers` instance
111
+ * satisfies it, and so does {@link headerReaderOf} over a plain object.
112
+ */
113
+ export interface HeaderReader {
114
+ get(name: string): string | null | undefined;
115
+ }
116
+ /** A header bag as a framework hands one over: values, or repeated values. */
117
+ export type HeaderRecord = Readonly<Record<string, string | readonly string[] | undefined>>;
118
+ /** The registry key for a charged Asset: `${chainId}:${address}`, lower-cased. */
119
+ export declare const chargedAssetKey: (asset: ChargedAsset) => string;
120
+ /** True when both refs name the same Asset on the same chain, spelling aside. */
121
+ export declare const sameChargedAsset: (a: ChargedAsset, b: ChargedAsset) => boolean;
122
+ /**
123
+ * Wraps a plain header object as a {@link HeaderReader}, matching names
124
+ * case-insensitively.
125
+ *
126
+ * Repeated values are joined with `, ` rather than resolved to one of them. Every
127
+ * Tab header is single-valued, so a repeat is a bug on the emitting side, and
128
+ * joining makes it fail the format check loudly instead of picking a winner.
129
+ */
130
+ export declare function headerReaderOf(source: HeaderReader | HeaderRecord): HeaderReader;
131
+ /**
132
+ * Parses a decimal integer count of Asset base units.
133
+ *
134
+ * `bigint` because base units are exact integers of arbitrary size, and a narrow
135
+ * grammar because every rejected spelling is a spelling that would otherwise be
136
+ * silently rounded or silently truncated.
137
+ */
138
+ export declare function parseBaseUnits(raw: string, header: string, code: string): Result<bigint>;
139
+ /** Renders `Tab-Charge-Asset`: `<chainId>:<address>`. */
140
+ export declare const formatChargedAsset: (asset: ChargedAsset) => string;
141
+ /** Parses `Tab-Charge-Asset`. */
142
+ export declare function parseChargedAsset(raw: string): Result<ChargedAsset>;
143
+ /**
144
+ * Reads the charge block off a response.
145
+ *
146
+ * Three outcomes, and the middle one is the reason this returns
147
+ * `Result<ChargeBlock | undefined>` rather than `Result<ChargeBlock>`:
148
+ *
149
+ * - all six headers present and well-formed, `ok(block)`;
150
+ * - none of the six present, `ok(undefined)`, an unmetered response;
151
+ * - some present, or one malformed, `err`, because a partial block cannot be
152
+ * completed by guessing and a zero is not a safe stand-in for an absent amount.
153
+ */
154
+ export declare function parseChargeHeaders(source: HeaderReader | HeaderRecord): Result<ChargeBlock | undefined>;
155
+ /**
156
+ * Renders a charge block as the six response headers.
157
+ *
158
+ * Exported for the emitting side, so the server plugin writes what this module's
159
+ * parser reads rather than what a second string-building routine believes. The
160
+ * round trip through {@link parseChargeHeaders} is asserted in the tests.
161
+ */
162
+ export declare function formatChargeHeaders(block: ChargeBlock): Result<Record<string, string>>;
163
+ /**
164
+ * Builds the request headers that identify the Agent being metered.
165
+ *
166
+ * `Tab-Agent` is the Agent's Monad address, its identity on the rail, and
167
+ * the only thing that tells a Service which tab to charge. `Tab-Authorisation` is
168
+ * optional here because the authKey is a per-Service arrangement a caller may not
169
+ * hold; a Service that requires one answers 403 through the ordinary error path
170
+ * rather than through this function.
171
+ */
172
+ export declare function agentRequestHeaders(agent: Address, authorisation?: Bytes32): Result<Record<string, string>>;
173
+ //# sourceMappingURL=headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/http/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEH,OAAO,EAKL,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAGvB,4DAA4D;AAC5D,eAAO,MAAM,UAAU;;;;;;;;;CASb,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,uBAAuB,6HAO1B,CAAC;AAEX;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,uFAAuF;AACvF,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CAC9C;AAED,8EAA8E;AAC9E,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;AAE5F,kFAAkF;AAClF,eAAO,MAAM,eAAe,GAAI,OAAO,YAAY,KAAG,MACU,CAAC;AAEjE,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,GAAI,GAAG,YAAY,EAAE,GAAG,YAAY,KAAG,OACzB,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,CAQhF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAexF;AAED,yDAAyD;AACzD,eAAO,MAAM,kBAAkB,GAAI,OAAO,YAAY,KAAG,MACO,CAAC;AAEjE,iCAAiC;AACjC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CA6BnE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,GAAG,YAAY,GAClC,MAAM,CAAC,WAAW,GAAG,SAAS,CAAC,CAwEjC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiDtF;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,aAAa,CAAC,EAAE,OAAO,GACtB,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBhC"}
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The Tab HTTP header contract, and the only parser and formatter for it.
3
+ *
4
+ * This module is the reconciliation point between the two halves of the wire
5
+ * protocol: the client wrapper in `client-402.ts` (R23.2) reads these headers off
6
+ * a Service response, and the server-side post-paid plugin writes them. Both
7
+ * halves go through the functions here rather than through their own string
8
+ * handling, so a disagreement about a format is a compile or test failure in one
9
+ * file instead of a silent mis-parse in production.
10
+ *
11
+ * ## The contract
12
+ *
13
+ * | Header | Direction | Required | Value format |
14
+ * | --- | --- | --- | --- |
15
+ * | `Tab-Charge-Amount` | response | yes, within the block | decimal integer Asset base units, no sign, no separators, no exponent |
16
+ * | `Tab-Charge-Asset` | response | yes, within the block | `<chainId>:<address>`: the decimal EVM chain id, a colon, then a `0x` 20-byte address |
17
+ * | `Tab-Charge-Service` | response | yes, within the block | `0x` 32-byte word: the `serviceId` |
18
+ * | `Tab-Charge-Tool` | response | yes, within the block | `0x` 32-byte word: the `bytes32` tool key from the applied price list |
19
+ * | `Tab-Open-Tab` | response | yes, within the block | decimal integer base units: the Open Tab for that Agent, Service, and Asset after this call |
20
+ * | `Tab-Headroom` | response | yes, within the block | decimal integer base units: headroom remaining for that Agent and Asset |
21
+ * | `Tab-Agent` | request | yes on a metered call | `0x` 20-byte address: the Agent's Monad address, its identity on the rail |
22
+ * | `Tab-Authorisation` | request | optional | `0x` 32-byte word: the `authKey` the Service should meter against |
23
+ *
24
+ * ## Five decisions the table does not show, each of which 15.3 must match
25
+ *
26
+ * **1. The six response headers are one all-or-nothing block.** A response
27
+ * carrying none of them is simply not a metered response, and
28
+ * {@link parseChargeHeaders} returns `ok(undefined)` for it, an unmetered health
29
+ * endpoint behind the same client must not become an error. A response carrying
30
+ * *some* of them is a malformed metered response and is an `err`, because the
31
+ * alternative is a client that silently records a charge against the wrong Asset
32
+ * or against no Asset at all. There is no partial block and no default value: a
33
+ * missing `Tab-Headroom` does not mean zero headroom.
34
+ *
35
+ * **2. Every amount is an integer count of Asset base units, parsed to
36
+ * `bigint`.** Never a `number`, never a decimal, never a rate. USDC is 6
37
+ * decimals, so `10_000` is one cent, and an Open Tab routinely exceeds the range
38
+ * a float represents exactly. `Number.parseInt` would round a large Open Tab into
39
+ * something that still looks plausible, which is worse than failing. The grammar
40
+ * is deliberately narrow, `/^[0-9]+$/`, so `1e6`, `1.0`, `1_000`, `+1`, `-1`,
41
+ * `0x10`, and `1,000` are all rejected rather than coerced.
42
+ *
43
+ * **3. `Tab-Charge-Asset` carries the chain id and the Asset address and nothing
44
+ * else.** Decimals and symbol are deliberately absent: they are registry facts,
45
+ * not per-call facts, and a Service that could restate an Asset's decimals on
46
+ * every response could restate what a base unit means. So the parsed
47
+ * {@link ChargedAsset} is a two-field reference rather than an `AssetRef`, and a
48
+ * caller that needs decimals resolves them from its own configuration.
49
+ *
50
+ * **4. The chain id is bounded as a `uint64` and not narrowed to Monad's two
51
+ * networks.** Refusing an unknown chain id here would close a set the
52
+ * payment-strategy seam deliberately leaves open (R23.6): a strategy settling on a
53
+ * network this release has never heard of should still be able to read its own
54
+ * charge headers.
55
+ *
56
+ * **5. The Asset address is lower-cased on parse, and `serviceId` and the tool
57
+ * key are not.** An address arrives checksummed from one source and lower-case
58
+ * from another, and two spellings of one Asset in a ledger keyed by Asset is a
59
+ * client that reports two Open Tabs where there is one. A `bytes32` word has no
60
+ * checksum convention to normalise, so it is carried through byte-for-byte.
61
+ *
62
+ * A `bytes32` tool key is usually a short name right-padded with zero bytes -
63
+ * `0x7461622e64656d6f…` is `tab.demo`, but nothing
64
+ * here decodes it. The key is what `ServiceRegistry.priceOf` is keyed on, and it
65
+ * is carried as the word it is so it round-trips into a contract call unchanged.
66
+ *
67
+ * Requirements: 23.2, 21.5
68
+ */
69
+ import { UINT64_MAX, isAddress, isBytes32, ok, } from "../_shared/index.js";
70
+ import { validationError } from "../errors.js";
71
+ /** Every Tab header name, in the casing this SDK writes. */
72
+ export const TAB_HEADER = {
73
+ chargeAmount: "Tab-Charge-Amount",
74
+ chargeAsset: "Tab-Charge-Asset",
75
+ chargeService: "Tab-Charge-Service",
76
+ chargeTool: "Tab-Charge-Tool",
77
+ openTab: "Tab-Open-Tab",
78
+ headroom: "Tab-Headroom",
79
+ agent: "Tab-Agent",
80
+ authorisation: "Tab-Authorisation",
81
+ };
82
+ /**
83
+ * The six response headers that form the charge block, all six required whenever
84
+ * any one of them is present.
85
+ */
86
+ export const CHARGE_RESPONSE_HEADERS = [
87
+ TAB_HEADER.chargeAmount,
88
+ TAB_HEADER.chargeAsset,
89
+ TAB_HEADER.chargeService,
90
+ TAB_HEADER.chargeTool,
91
+ TAB_HEADER.openTab,
92
+ TAB_HEADER.headroom,
93
+ ];
94
+ /** The registry key for a charged Asset: `${chainId}:${address}`, lower-cased. */
95
+ export const chargedAssetKey = (asset) => `${asset.chainId.toString(10)}:${asset.address.toLowerCase()}`;
96
+ /** True when both refs name the same Asset on the same chain, spelling aside. */
97
+ export const sameChargedAsset = (a, b) => chargedAssetKey(a) === chargedAssetKey(b);
98
+ /**
99
+ * Wraps a plain header object as a {@link HeaderReader}, matching names
100
+ * case-insensitively.
101
+ *
102
+ * Repeated values are joined with `, ` rather than resolved to one of them. Every
103
+ * Tab header is single-valued, so a repeat is a bug on the emitting side, and
104
+ * joining makes it fail the format check loudly instead of picking a winner.
105
+ */
106
+ export function headerReaderOf(source) {
107
+ if (typeof source.get === "function")
108
+ return source;
109
+ const indexed = new Map();
110
+ for (const [name, value] of Object.entries(source)) {
111
+ if (value === undefined)
112
+ continue;
113
+ indexed.set(name.toLowerCase(), Array.isArray(value) ? value.join(", ") : value);
114
+ }
115
+ return { get: (name) => indexed.get(name.toLowerCase()) ?? null };
116
+ }
117
+ /**
118
+ * Parses a decimal integer count of Asset base units.
119
+ *
120
+ * `bigint` because base units are exact integers of arbitrary size, and a narrow
121
+ * grammar because every rejected spelling is a spelling that would otherwise be
122
+ * silently rounded or silently truncated.
123
+ */
124
+ export function parseBaseUnits(raw, header, code) {
125
+ const value = raw.trim();
126
+ if (value.length === 0) {
127
+ return validationError(code, `${header} is empty; it must carry a decimal integer of base units`, {
128
+ details: { header },
129
+ });
130
+ }
131
+ if (!/^[0-9]+$/.test(value)) {
132
+ return validationError(code, `${header} carries \`${value}\`, which is not a decimal integer of base units; no sign, separator, decimal point, exponent, or 0x prefix is accepted`, { details: { header, value } });
133
+ }
134
+ return ok(BigInt(value));
135
+ }
136
+ /** Renders `Tab-Charge-Asset`: `<chainId>:<address>`. */
137
+ export const formatChargedAsset = (asset) => `${asset.chainId.toString(10)}:${asset.address.toLowerCase()}`;
138
+ /** Parses `Tab-Charge-Asset`. */
139
+ export function parseChargedAsset(raw) {
140
+ const value = raw.trim();
141
+ const parts = value.split(":");
142
+ const [chainIdPart, addressPart] = parts;
143
+ if (parts.length !== 2 || chainIdPart === undefined || addressPart === undefined) {
144
+ return validationError("CHARGE_ASSET_INVALID", `${TAB_HEADER.chargeAsset} carries \`${value}\`; it must be exactly \`<chainId>:<address>\``, { details: { header: TAB_HEADER.chargeAsset, value } });
145
+ }
146
+ const chainId = parseBaseUnits(chainIdPart, `${TAB_HEADER.chargeAsset} chainId`, "CHARGE_ASSET_INVALID");
147
+ if (!chainId.ok)
148
+ return chainId;
149
+ if (chainId.value > UINT64_MAX) {
150
+ return validationError("CHARGE_ASSET_INVALID", `${TAB_HEADER.chargeAsset} names chain id ${chainId.value.toString(10)}, which exceeds uint64`, { details: { header: TAB_HEADER.chargeAsset, value } });
151
+ }
152
+ const address = addressPart.trim();
153
+ if (!isAddress(address)) {
154
+ return validationError("CHARGE_ASSET_INVALID", `${TAB_HEADER.chargeAsset} names \`${address}\` as its Asset, which is not a 20-byte 0x address`, { details: { header: TAB_HEADER.chargeAsset, value } });
155
+ }
156
+ return ok({ chainId: chainId.value, address: address.toLowerCase() });
157
+ }
158
+ /**
159
+ * Reads the charge block off a response.
160
+ *
161
+ * Three outcomes, and the middle one is the reason this returns
162
+ * `Result<ChargeBlock | undefined>` rather than `Result<ChargeBlock>`:
163
+ *
164
+ * - all six headers present and well-formed, `ok(block)`;
165
+ * - none of the six present, `ok(undefined)`, an unmetered response;
166
+ * - some present, or one malformed, `err`, because a partial block cannot be
167
+ * completed by guessing and a zero is not a safe stand-in for an absent amount.
168
+ */
169
+ export function parseChargeHeaders(source) {
170
+ const headers = headerReaderOf(source);
171
+ const raw = new Map();
172
+ const absent = [];
173
+ for (const name of CHARGE_RESPONSE_HEADERS) {
174
+ const value = headers.get(name);
175
+ if (value === null || value === undefined || value.trim().length === 0) {
176
+ absent.push(name);
177
+ continue;
178
+ }
179
+ raw.set(name, value);
180
+ }
181
+ if (absent.length === CHARGE_RESPONSE_HEADERS.length)
182
+ return ok(undefined);
183
+ if (absent.length > 0) {
184
+ return validationError("CHARGE_HEADERS_INCOMPLETE", `the response carries part of a Tab charge block and is missing ${absent.join(", ")}; the ${CHARGE_RESPONSE_HEADERS.length} charge headers are all-or-nothing, because a missing header is not a zero`, { details: { missing: absent.join(", "), present: [...raw.keys()].join(", ") } });
185
+ }
186
+ const amount = parseBaseUnits(raw.get(TAB_HEADER.chargeAmount) ?? "", TAB_HEADER.chargeAmount, "CHARGE_AMOUNT_INVALID");
187
+ if (!amount.ok)
188
+ return amount;
189
+ const asset = parseChargedAsset(raw.get(TAB_HEADER.chargeAsset) ?? "");
190
+ if (!asset.ok)
191
+ return asset;
192
+ const serviceId = (raw.get(TAB_HEADER.chargeService) ?? "").trim();
193
+ if (!isBytes32(serviceId)) {
194
+ return validationError("CHARGE_SERVICE_INVALID", `${TAB_HEADER.chargeService} carries \`${serviceId}\`, which is not a 32-byte 0x word`, { details: { header: TAB_HEADER.chargeService, value: serviceId } });
195
+ }
196
+ const tool = (raw.get(TAB_HEADER.chargeTool) ?? "").trim();
197
+ if (!isBytes32(tool)) {
198
+ return validationError("CHARGE_TOOL_INVALID", `${TAB_HEADER.chargeTool} carries \`${tool}\`, which is not a 32-byte 0x word; the tool key is the bytes32 the price list is keyed on, not its decoded name`, { details: { header: TAB_HEADER.chargeTool, value: tool } });
199
+ }
200
+ const openTab = parseBaseUnits(raw.get(TAB_HEADER.openTab) ?? "", TAB_HEADER.openTab, "OPEN_TAB_INVALID");
201
+ if (!openTab.ok)
202
+ return openTab;
203
+ const headroom = parseBaseUnits(raw.get(TAB_HEADER.headroom) ?? "", TAB_HEADER.headroom, "HEADROOM_INVALID");
204
+ if (!headroom.ok)
205
+ return headroom;
206
+ return ok({
207
+ amount: amount.value,
208
+ asset: asset.value,
209
+ serviceId,
210
+ tool,
211
+ openTab: openTab.value,
212
+ headroom: headroom.value,
213
+ });
214
+ }
215
+ /**
216
+ * Renders a charge block as the six response headers.
217
+ *
218
+ * Exported for the emitting side, so the server plugin writes what this module's
219
+ * parser reads rather than what a second string-building routine believes. The
220
+ * round trip through {@link parseChargeHeaders} is asserted in the tests.
221
+ */
222
+ export function formatChargeHeaders(block) {
223
+ for (const [label, value, code] of [
224
+ [TAB_HEADER.chargeAmount, block.amount, "CHARGE_AMOUNT_INVALID"],
225
+ [TAB_HEADER.openTab, block.openTab, "OPEN_TAB_INVALID"],
226
+ [TAB_HEADER.headroom, block.headroom, "HEADROOM_INVALID"],
227
+ ]) {
228
+ if (typeof value !== "bigint") {
229
+ return validationError(code, `${label} must be a bigint count of base units, never a number`, {
230
+ details: { header: label },
231
+ });
232
+ }
233
+ if (value < 0n) {
234
+ return validationError(code, `${label} must not be negative, received ${value.toString(10)}`, {
235
+ details: { header: label },
236
+ });
237
+ }
238
+ }
239
+ if (typeof block.asset.chainId !== "bigint" || block.asset.chainId < 0n) {
240
+ return validationError("CHARGE_ASSET_INVALID", `${TAB_HEADER.chargeAsset} needs a non-negative bigint chain id`);
241
+ }
242
+ if (block.asset.chainId > UINT64_MAX) {
243
+ return validationError("CHARGE_ASSET_INVALID", `${TAB_HEADER.chargeAsset} chain id exceeds uint64: ${block.asset.chainId.toString(10)}`);
244
+ }
245
+ if (!isAddress(block.asset.address)) {
246
+ return validationError("CHARGE_ASSET_INVALID", `${TAB_HEADER.chargeAsset} needs a 20-byte 0x Asset address`);
247
+ }
248
+ if (!isBytes32(block.serviceId)) {
249
+ return validationError("CHARGE_SERVICE_INVALID", `${TAB_HEADER.chargeService} needs a 32-byte 0x word`);
250
+ }
251
+ if (!isBytes32(block.tool)) {
252
+ return validationError("CHARGE_TOOL_INVALID", `${TAB_HEADER.chargeTool} needs a 32-byte 0x word`);
253
+ }
254
+ return ok({
255
+ [TAB_HEADER.chargeAmount]: block.amount.toString(10),
256
+ [TAB_HEADER.chargeAsset]: formatChargedAsset(block.asset),
257
+ [TAB_HEADER.chargeService]: block.serviceId,
258
+ [TAB_HEADER.chargeTool]: block.tool,
259
+ [TAB_HEADER.openTab]: block.openTab.toString(10),
260
+ [TAB_HEADER.headroom]: block.headroom.toString(10),
261
+ });
262
+ }
263
+ /**
264
+ * Builds the request headers that identify the Agent being metered.
265
+ *
266
+ * `Tab-Agent` is the Agent's Monad address, its identity on the rail, and
267
+ * the only thing that tells a Service which tab to charge. `Tab-Authorisation` is
268
+ * optional here because the authKey is a per-Service arrangement a caller may not
269
+ * hold; a Service that requires one answers 403 through the ordinary error path
270
+ * rather than through this function.
271
+ */
272
+ export function agentRequestHeaders(agent, authorisation) {
273
+ if (!isAddress(agent)) {
274
+ return validationError("AGENT_INVALID", `${TAB_HEADER.agent} must be the Agent's 20-byte 0x Monad address, received \`${String(agent)}\``);
275
+ }
276
+ if (authorisation !== undefined && !isBytes32(authorisation)) {
277
+ return validationError("AUTHORISATION_INVALID", `${TAB_HEADER.authorisation} must be a 32-byte 0x authKey, received \`${String(authorisation)}\``);
278
+ }
279
+ return ok({
280
+ [TAB_HEADER.agent]: agent,
281
+ ...(authorisation === undefined ? {} : { [TAB_HEADER.authorisation]: authorisation }),
282
+ });
283
+ }
284
+ //# sourceMappingURL=headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/http/headers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The Tab HTTP header contract, and the only parser and formatter for it.\n *\n * This module is the reconciliation point between the two halves of the wire\n * protocol: the client wrapper in `client-402.ts` (R23.2) reads these headers off\n * a Service response, and the server-side post-paid plugin writes them. Both\n * halves go through the functions here rather than through their own string\n * handling, so a disagreement about a format is a compile or test failure in one\n * file instead of a silent mis-parse in production.\n *\n * ## The contract\n *\n * | Header | Direction | Required | Value format |\n * | --- | --- | --- | --- |\n * | `Tab-Charge-Amount` | response | yes, within the block | decimal integer Asset base units, no sign, no separators, no exponent |\n * | `Tab-Charge-Asset` | response | yes, within the block | `<chainId>:<address>`: the decimal EVM chain id, a colon, then a `0x` 20-byte address |\n * | `Tab-Charge-Service` | response | yes, within the block | `0x` 32-byte word: the `serviceId` |\n * | `Tab-Charge-Tool` | response | yes, within the block | `0x` 32-byte word: the `bytes32` tool key from the applied price list |\n * | `Tab-Open-Tab` | response | yes, within the block | decimal integer base units: the Open Tab for that Agent, Service, and Asset after this call |\n * | `Tab-Headroom` | response | yes, within the block | decimal integer base units: headroom remaining for that Agent and Asset |\n * | `Tab-Agent` | request | yes on a metered call | `0x` 20-byte address: the Agent's Monad address, its identity on the rail |\n * | `Tab-Authorisation` | request | optional | `0x` 32-byte word: the `authKey` the Service should meter against |\n *\n * ## Five decisions the table does not show, each of which 15.3 must match\n *\n * **1. The six response headers are one all-or-nothing block.** A response\n * carrying none of them is simply not a metered response, and\n * {@link parseChargeHeaders} returns `ok(undefined)` for it, an unmetered health\n * endpoint behind the same client must not become an error. A response carrying\n * *some* of them is a malformed metered response and is an `err`, because the\n * alternative is a client that silently records a charge against the wrong Asset\n * or against no Asset at all. There is no partial block and no default value: a\n * missing `Tab-Headroom` does not mean zero headroom.\n *\n * **2. Every amount is an integer count of Asset base units, parsed to\n * `bigint`.** Never a `number`, never a decimal, never a rate. USDC is 6\n * decimals, so `10_000` is one cent, and an Open Tab routinely exceeds the range\n * a float represents exactly. `Number.parseInt` would round a large Open Tab into\n * something that still looks plausible, which is worse than failing. The grammar\n * is deliberately narrow, `/^[0-9]+$/`, so `1e6`, `1.0`, `1_000`, `+1`, `-1`,\n * `0x10`, and `1,000` are all rejected rather than coerced.\n *\n * **3. `Tab-Charge-Asset` carries the chain id and the Asset address and nothing\n * else.** Decimals and symbol are deliberately absent: they are registry facts,\n * not per-call facts, and a Service that could restate an Asset's decimals on\n * every response could restate what a base unit means. So the parsed\n * {@link ChargedAsset} is a two-field reference rather than an `AssetRef`, and a\n * caller that needs decimals resolves them from its own configuration.\n *\n * **4. The chain id is bounded as a `uint64` and not narrowed to Monad's two\n * networks.** Refusing an unknown chain id here would close a set the\n * payment-strategy seam deliberately leaves open (R23.6): a strategy settling on a\n * network this release has never heard of should still be able to read its own\n * charge headers.\n *\n * **5. The Asset address is lower-cased on parse, and `serviceId` and the tool\n * key are not.** An address arrives checksummed from one source and lower-case\n * from another, and two spellings of one Asset in a ledger keyed by Asset is a\n * client that reports two Open Tabs where there is one. A `bytes32` word has no\n * checksum convention to normalise, so it is carried through byte-for-byte.\n *\n * A `bytes32` tool key is usually a short name right-padded with zero bytes -\n * `0x7461622e64656d6f…` is `tab.demo`, but nothing\n * here decodes it. The key is what `ServiceRegistry.priceOf` is keyed on, and it\n * is carried as the word it is so it round-trips into a contract call unchanged.\n *\n * Requirements: 23.2, 21.5\n */\n\nimport {\n UINT64_MAX,\n isAddress,\n isBytes32,\n ok,\n type Address,\n type Bytes32,\n type Result,\n} from \"../_shared/index.js\";\nimport { validationError } from \"../errors.js\";\n\n/** Every Tab header name, in the casing this SDK writes. */\nexport const TAB_HEADER = {\n chargeAmount: \"Tab-Charge-Amount\",\n chargeAsset: \"Tab-Charge-Asset\",\n chargeService: \"Tab-Charge-Service\",\n chargeTool: \"Tab-Charge-Tool\",\n openTab: \"Tab-Open-Tab\",\n headroom: \"Tab-Headroom\",\n agent: \"Tab-Agent\",\n authorisation: \"Tab-Authorisation\",\n} as const;\n\n/**\n * The six response headers that form the charge block, all six required whenever\n * any one of them is present.\n */\nexport const CHARGE_RESPONSE_HEADERS = [\n TAB_HEADER.chargeAmount,\n TAB_HEADER.chargeAsset,\n TAB_HEADER.chargeService,\n TAB_HEADER.chargeTool,\n TAB_HEADER.openTab,\n TAB_HEADER.headroom,\n] as const;\n\n/**\n * The Asset a charge is denominated in, exactly as `Tab-Charge-Asset` carries it:\n * a chain id and an address, with no decimals and no symbol.\n */\nexport interface ChargedAsset {\n /** The EVM chain id the Asset lives on: 143 for Monad Mainnet, 10143 for Monad Testnet. */\n readonly chainId: bigint;\n /** Lower-cased on parse, so one Asset has one spelling in a ledger keyed by it. */\n readonly address: Address;\n}\n\n/** One decoded charge block: what the Service says this call cost and what it left. */\nexport interface ChargeBlock {\n /** Integer Asset base units for this call. */\n readonly amount: bigint;\n readonly asset: ChargedAsset;\n readonly serviceId: Bytes32;\n /** The `bytes32` tool key from the applied price list. */\n readonly tool: Bytes32;\n /** The Open Tab for this Agent, Service, and Asset after this call. */\n readonly openTab: bigint;\n /** Headroom remaining for this Agent and Asset. */\n readonly headroom: bigint;\n}\n\n/**\n * Anything that answers a case-insensitive header lookup. A `Headers` instance\n * satisfies it, and so does {@link headerReaderOf} over a plain object.\n */\nexport interface HeaderReader {\n get(name: string): string | null | undefined;\n}\n\n/** A header bag as a framework hands one over: values, or repeated values. */\nexport type HeaderRecord = Readonly<Record<string, string | readonly string[] | undefined>>;\n\n/** The registry key for a charged Asset: `${chainId}:${address}`, lower-cased. */\nexport const chargedAssetKey = (asset: ChargedAsset): string =>\n `${asset.chainId.toString(10)}:${asset.address.toLowerCase()}`;\n\n/** True when both refs name the same Asset on the same chain, spelling aside. */\nexport const sameChargedAsset = (a: ChargedAsset, b: ChargedAsset): boolean =>\n chargedAssetKey(a) === chargedAssetKey(b);\n\n/**\n * Wraps a plain header object as a {@link HeaderReader}, matching names\n * case-insensitively.\n *\n * Repeated values are joined with `, ` rather than resolved to one of them. Every\n * Tab header is single-valued, so a repeat is a bug on the emitting side, and\n * joining makes it fail the format check loudly instead of picking a winner.\n */\nexport function headerReaderOf(source: HeaderReader | HeaderRecord): HeaderReader {\n if (typeof (source as HeaderReader).get === \"function\") return source as HeaderReader;\n const indexed = new Map<string, string>();\n for (const [name, value] of Object.entries(source as HeaderRecord)) {\n if (value === undefined) continue;\n indexed.set(name.toLowerCase(), Array.isArray(value) ? value.join(\", \") : (value as string));\n }\n return { get: (name) => indexed.get(name.toLowerCase()) ?? null };\n}\n\n/**\n * Parses a decimal integer count of Asset base units.\n *\n * `bigint` because base units are exact integers of arbitrary size, and a narrow\n * grammar because every rejected spelling is a spelling that would otherwise be\n * silently rounded or silently truncated.\n */\nexport function parseBaseUnits(raw: string, header: string, code: string): Result<bigint> {\n const value = raw.trim();\n if (value.length === 0) {\n return validationError(code, `${header} is empty; it must carry a decimal integer of base units`, {\n details: { header },\n });\n }\n if (!/^[0-9]+$/.test(value)) {\n return validationError(\n code,\n `${header} carries \\`${value}\\`, which is not a decimal integer of base units; no sign, separator, decimal point, exponent, or 0x prefix is accepted`,\n { details: { header, value } },\n );\n }\n return ok(BigInt(value));\n}\n\n/** Renders `Tab-Charge-Asset`: `<chainId>:<address>`. */\nexport const formatChargedAsset = (asset: ChargedAsset): string =>\n `${asset.chainId.toString(10)}:${asset.address.toLowerCase()}`;\n\n/** Parses `Tab-Charge-Asset`. */\nexport function parseChargedAsset(raw: string): Result<ChargedAsset> {\n const value = raw.trim();\n const parts = value.split(\":\");\n const [chainIdPart, addressPart] = parts;\n if (parts.length !== 2 || chainIdPart === undefined || addressPart === undefined) {\n return validationError(\n \"CHARGE_ASSET_INVALID\",\n `${TAB_HEADER.chargeAsset} carries \\`${value}\\`; it must be exactly \\`<chainId>:<address>\\``,\n { details: { header: TAB_HEADER.chargeAsset, value } },\n );\n }\n const chainId = parseBaseUnits(chainIdPart, `${TAB_HEADER.chargeAsset} chainId`, \"CHARGE_ASSET_INVALID\");\n if (!chainId.ok) return chainId;\n if (chainId.value > UINT64_MAX) {\n return validationError(\n \"CHARGE_ASSET_INVALID\",\n `${TAB_HEADER.chargeAsset} names chain id ${chainId.value.toString(10)}, which exceeds uint64`,\n { details: { header: TAB_HEADER.chargeAsset, value } },\n );\n }\n const address = addressPart.trim();\n if (!isAddress(address)) {\n return validationError(\n \"CHARGE_ASSET_INVALID\",\n `${TAB_HEADER.chargeAsset} names \\`${address}\\` as its Asset, which is not a 20-byte 0x address`,\n { details: { header: TAB_HEADER.chargeAsset, value } },\n );\n }\n return ok({ chainId: chainId.value, address: address.toLowerCase() as Address });\n}\n\n/**\n * Reads the charge block off a response.\n *\n * Three outcomes, and the middle one is the reason this returns\n * `Result<ChargeBlock | undefined>` rather than `Result<ChargeBlock>`:\n *\n * - all six headers present and well-formed, `ok(block)`;\n * - none of the six present, `ok(undefined)`, an unmetered response;\n * - some present, or one malformed, `err`, because a partial block cannot be\n * completed by guessing and a zero is not a safe stand-in for an absent amount.\n */\nexport function parseChargeHeaders(\n source: HeaderReader | HeaderRecord,\n): Result<ChargeBlock | undefined> {\n const headers = headerReaderOf(source);\n const raw = new Map<string, string>();\n const absent: string[] = [];\n for (const name of CHARGE_RESPONSE_HEADERS) {\n const value = headers.get(name);\n if (value === null || value === undefined || value.trim().length === 0) {\n absent.push(name);\n continue;\n }\n raw.set(name, value);\n }\n\n if (absent.length === CHARGE_RESPONSE_HEADERS.length) return ok(undefined);\n if (absent.length > 0) {\n return validationError(\n \"CHARGE_HEADERS_INCOMPLETE\",\n `the response carries part of a Tab charge block and is missing ${absent.join(\", \")}; the ${CHARGE_RESPONSE_HEADERS.length} charge headers are all-or-nothing, because a missing header is not a zero`,\n { details: { missing: absent.join(\", \"), present: [...raw.keys()].join(\", \") } },\n );\n }\n\n const amount = parseBaseUnits(\n raw.get(TAB_HEADER.chargeAmount) ?? \"\",\n TAB_HEADER.chargeAmount,\n \"CHARGE_AMOUNT_INVALID\",\n );\n if (!amount.ok) return amount;\n\n const asset = parseChargedAsset(raw.get(TAB_HEADER.chargeAsset) ?? \"\");\n if (!asset.ok) return asset;\n\n const serviceId = (raw.get(TAB_HEADER.chargeService) ?? \"\").trim();\n if (!isBytes32(serviceId)) {\n return validationError(\n \"CHARGE_SERVICE_INVALID\",\n `${TAB_HEADER.chargeService} carries \\`${serviceId}\\`, which is not a 32-byte 0x word`,\n { details: { header: TAB_HEADER.chargeService, value: serviceId } },\n );\n }\n\n const tool = (raw.get(TAB_HEADER.chargeTool) ?? \"\").trim();\n if (!isBytes32(tool)) {\n return validationError(\n \"CHARGE_TOOL_INVALID\",\n `${TAB_HEADER.chargeTool} carries \\`${tool}\\`, which is not a 32-byte 0x word; the tool key is the bytes32 the price list is keyed on, not its decoded name`,\n { details: { header: TAB_HEADER.chargeTool, value: tool } },\n );\n }\n\n const openTab = parseBaseUnits(\n raw.get(TAB_HEADER.openTab) ?? \"\",\n TAB_HEADER.openTab,\n \"OPEN_TAB_INVALID\",\n );\n if (!openTab.ok) return openTab;\n\n const headroom = parseBaseUnits(\n raw.get(TAB_HEADER.headroom) ?? \"\",\n TAB_HEADER.headroom,\n \"HEADROOM_INVALID\",\n );\n if (!headroom.ok) return headroom;\n\n return ok({\n amount: amount.value,\n asset: asset.value,\n serviceId,\n tool,\n openTab: openTab.value,\n headroom: headroom.value,\n });\n}\n\n/**\n * Renders a charge block as the six response headers.\n *\n * Exported for the emitting side, so the server plugin writes what this module's\n * parser reads rather than what a second string-building routine believes. The\n * round trip through {@link parseChargeHeaders} is asserted in the tests.\n */\nexport function formatChargeHeaders(block: ChargeBlock): Result<Record<string, string>> {\n for (const [label, value, code] of [\n [TAB_HEADER.chargeAmount, block.amount, \"CHARGE_AMOUNT_INVALID\"],\n [TAB_HEADER.openTab, block.openTab, \"OPEN_TAB_INVALID\"],\n [TAB_HEADER.headroom, block.headroom, \"HEADROOM_INVALID\"],\n ] as const) {\n if (typeof value !== \"bigint\") {\n return validationError(code, `${label} must be a bigint count of base units, never a number`, {\n details: { header: label },\n });\n }\n if (value < 0n) {\n return validationError(code, `${label} must not be negative, received ${value.toString(10)}`, {\n details: { header: label },\n });\n }\n }\n if (typeof block.asset.chainId !== \"bigint\" || block.asset.chainId < 0n) {\n return validationError(\n \"CHARGE_ASSET_INVALID\",\n `${TAB_HEADER.chargeAsset} needs a non-negative bigint chain id`,\n );\n }\n if (block.asset.chainId > UINT64_MAX) {\n return validationError(\n \"CHARGE_ASSET_INVALID\",\n `${TAB_HEADER.chargeAsset} chain id exceeds uint64: ${block.asset.chainId.toString(10)}`,\n );\n }\n if (!isAddress(block.asset.address)) {\n return validationError(\n \"CHARGE_ASSET_INVALID\",\n `${TAB_HEADER.chargeAsset} needs a 20-byte 0x Asset address`,\n );\n }\n if (!isBytes32(block.serviceId)) {\n return validationError(\"CHARGE_SERVICE_INVALID\", `${TAB_HEADER.chargeService} needs a 32-byte 0x word`);\n }\n if (!isBytes32(block.tool)) {\n return validationError(\"CHARGE_TOOL_INVALID\", `${TAB_HEADER.chargeTool} needs a 32-byte 0x word`);\n }\n return ok({\n [TAB_HEADER.chargeAmount]: block.amount.toString(10),\n [TAB_HEADER.chargeAsset]: formatChargedAsset(block.asset),\n [TAB_HEADER.chargeService]: block.serviceId,\n [TAB_HEADER.chargeTool]: block.tool,\n [TAB_HEADER.openTab]: block.openTab.toString(10),\n [TAB_HEADER.headroom]: block.headroom.toString(10),\n });\n}\n\n/**\n * Builds the request headers that identify the Agent being metered.\n *\n * `Tab-Agent` is the Agent's Monad address, its identity on the rail, and\n * the only thing that tells a Service which tab to charge. `Tab-Authorisation` is\n * optional here because the authKey is a per-Service arrangement a caller may not\n * hold; a Service that requires one answers 403 through the ordinary error path\n * rather than through this function.\n */\nexport function agentRequestHeaders(\n agent: Address,\n authorisation?: Bytes32,\n): Result<Record<string, string>> {\n if (!isAddress(agent)) {\n return validationError(\n \"AGENT_INVALID\",\n `${TAB_HEADER.agent} must be the Agent's 20-byte 0x Monad address, received \\`${String(agent)}\\``,\n );\n }\n if (authorisation !== undefined && !isBytes32(authorisation)) {\n return validationError(\n \"AUTHORISATION_INVALID\",\n `${TAB_HEADER.authorisation} must be a 32-byte 0x authKey, received \\`${String(authorisation)}\\``,\n );\n }\n return ok({\n [TAB_HEADER.agent]: agent,\n ...(authorisation === undefined ? {} : { [TAB_HEADER.authorisation]: authorisation }),\n });\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The HTTP surface of the SDK: the header contract, and the post-paid 402 client
3
+ * that reads it.
4
+ *
5
+ * `headers.ts` is the shared half. It is the only definition of the `Tab-*` wire
6
+ * format, and both directions go through it, the client here parses with it, and
7
+ * the server-side post-paid plugin formats with it, so the two halves cannot
8
+ * drift apart without a test failing.
9
+ *
10
+ * Requirements: 23.2, 21.5
11
+ */
12
+ export * from "./headers.js";
13
+ export * from "./client-402.js";
14
+ export * from "./metering-claim.js";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The HTTP surface of the SDK: the header contract, and the post-paid 402 client
3
+ * that reads it.
4
+ *
5
+ * `headers.ts` is the shared half. It is the only definition of the `Tab-*` wire
6
+ * format, and both directions go through it, the client here parses with it, and
7
+ * the server-side post-paid plugin formats with it, so the two halves cannot
8
+ * drift apart without a test failing.
9
+ *
10
+ * Requirements: 23.2, 21.5
11
+ */
12
+ export * from "./headers.js";
13
+ export * from "./client-402.js";
14
+ export * from "./metering-claim.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC","sourcesContent":["/**\n * The HTTP surface of the SDK: the header contract, and the post-paid 402 client\n * that reads it.\n *\n * `headers.ts` is the shared half. It is the only definition of the `Tab-*` wire\n * format, and both directions go through it, the client here parses with it, and\n * the server-side post-paid plugin formats with it, so the two halves cannot\n * drift apart without a test failing.\n *\n * Requirements: 23.2, 21.5\n */\n\nexport * from \"./headers.js\";\nexport * from \"./client-402.js\";\nexport * from \"./metering-claim.js\";\n"]}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The metering claim: what a signed metered request binds, and who may sign it.
3
+ *
4
+ * ## Two signers, one digest
5
+ *
6
+ * A metered call records a delivery on chain and spends the Service operator's
7
+ * gas, so a metering endpoint anyone can reach must know the call was meant.
8
+ * Two parties can say so, and the digest they sign is the same string:
9
+ *
10
+ * - **The operator**, from the Service's own front, which has authenticated
11
+ * its caller however it likes. `Tab-Operator-Signature` carries it.
12
+ * - **The Agent**, whose Open Tab the charge lands on. `Tab-Agent-Signature`
13
+ * carries it, recovered against the address in `Tab-Agent`, so the one
14
+ * party that pays for the call is the one that asked for it, and a
15
+ * stranger who knows an Agent's address can put nothing on its tab.
16
+ *
17
+ * The digest binds the method, the path, the Agent, the tool, the unit count
18
+ * and a timestamp. Binding the Agent and the units is the point: a signature
19
+ * over the path alone would be a bearer token that replays against a
20
+ * different Agent for a different amount. The timestamp bounds replay to a
21
+ * window the gateway checks.
22
+ *
23
+ * ## Why it is written out and not JSON
24
+ *
25
+ * Newline-separated and fully ordered, so two different claims can never
26
+ * produce one digest. JSON key order is not guaranteed across
27
+ * implementations, and a signature over a reordered object would fail for a
28
+ * caller who did nothing wrong.
29
+ *
30
+ * Requirements: 12.1, 12.2, 21.5
31
+ */
32
+ import type { ServiceHeaderProvider } from "../payments/config.js";
33
+ /** The headers a signed metering request carries, by who signed it. */
34
+ export declare const METERING_HEADER: {
35
+ readonly operatorSignature: "Tab-Operator-Signature";
36
+ readonly operatorIssuedAt: "Tab-Operator-Issued-At";
37
+ readonly agentSignature: "Tab-Agent-Signature";
38
+ readonly agentIssuedAt: "Tab-Agent-Issued-At";
39
+ };
40
+ /** The fields a metering request signature binds. */
41
+ export interface MeteringRequestClaim {
42
+ readonly method: string;
43
+ readonly path: string;
44
+ readonly agent: string;
45
+ /** The 32-byte tool key the price list is keyed by, never the label. */
46
+ readonly tool: string;
47
+ readonly units: number;
48
+ /** Milliseconds since the epoch, as the caller stated it. */
49
+ readonly issuedAt: number;
50
+ }
51
+ /** The exact string a signer signs, with EIP-191 `personal_sign`. */
52
+ export declare function meteringDigest(claim: MeteringRequestClaim): string;
53
+ /** A signer that can `personal_sign`. An ethers `Wallet` satisfies it. */
54
+ export interface MeteringSigner {
55
+ getAddress(): Promise<string>;
56
+ signMessage(message: string): Promise<string>;
57
+ }
58
+ /** A tool name as the caller gave it, packed to the key the price list holds, unless it already is one. */
59
+ export declare function toolKeyOf(tool: string): string;
60
+ /**
61
+ * A header provider that signs every metered call as the Agent.
62
+ *
63
+ * Put it on a Service entry in `tab.config` and `tab_call` sends
64
+ * `Tab-Agent-Signature` and `Tab-Agent-Issued-At` with each call, signed by
65
+ * the key the factory returns. A factory, like the strategies, so the key is
66
+ * built only when a call is made and every read stays keyless; one that
67
+ * returns nothing sends the call unsigned, and a gateway that requires a
68
+ * signature says so in its refusal.
69
+ *
70
+ * It signs only when the key it is given is the Agent the call is metered
71
+ * against, because that is what the gateway recovers the signature against.
72
+ * A key for anyone else adds nothing and the Service decides: one that
73
+ * requires a signature refuses by name, and one that does not is unaffected.
74
+ * That is the case where the Agent comes from somewhere other than this
75
+ * config, such as a wallet the MetaMask Agent Wallet plugin reads, and it is
76
+ * an honest "this key is not that Agent" rather than a signature that could
77
+ * only be rejected.
78
+ */
79
+ export declare function agentSignedMetering(signer: () => MeteringSigner | undefined, options?: {
80
+ readonly now?: () => number;
81
+ }): ServiceHeaderProvider;
82
+ //# sourceMappingURL=metering-claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metering-claim.d.ts","sourceRoot":"","sources":["../../src/http/metering-claim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,OAAO,KAAK,EAAE,qBAAqB,EAAwB,MAAM,uBAAuB,CAAC;AAEzF,uEAAuE;AACvE,eAAO,MAAM,eAAe;;;;;CAKlB,CAAC;AAEX,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAUlE;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/C;AAED,2GAA2G;AAC3G,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,cAAc,GAAG,SAAS,EACxC,OAAO,GAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GAC5C,qBAAqB,CAqBvB"}