@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.
- package/LICENSE +23 -0
- package/README.md +401 -0
- package/bin/tab.mjs +23 -0
- package/dist/_shared/abi.d.ts +150 -0
- package/dist/_shared/abi.d.ts.map +1 -0
- package/dist/_shared/abi.js +197 -0
- package/dist/_shared/abi.js.map +1 -0
- package/dist/_shared/chains.d.ts +118 -0
- package/dist/_shared/chains.d.ts.map +1 -0
- package/dist/_shared/chains.js +89 -0
- package/dist/_shared/chains.js.map +1 -0
- package/dist/_shared/hex.d.ts +35 -0
- package/dist/_shared/hex.d.ts.map +1 -0
- package/dist/_shared/hex.js +40 -0
- package/dist/_shared/hex.js.map +1 -0
- package/dist/_shared/index.d.ts +14 -0
- package/dist/_shared/index.d.ts.map +1 -0
- package/dist/_shared/index.js +14 -0
- package/dist/_shared/index.js.map +1 -0
- package/dist/_shared/keccak256.d.ts +29 -0
- package/dist/_shared/keccak256.d.ts.map +1 -0
- package/dist/_shared/keccak256.js +145 -0
- package/dist/_shared/keccak256.js.map +1 -0
- package/dist/_shared/result.d.ts +78 -0
- package/dist/_shared/result.d.ts.map +1 -0
- package/dist/_shared/result.js +61 -0
- package/dist/_shared/result.js.map +1 -0
- package/dist/cli/client-config.d.ts +155 -0
- package/dist/cli/client-config.d.ts.map +1 -0
- package/dist/cli/client-config.js +382 -0
- package/dist/cli/client-config.js.map +1 -0
- package/dist/cli/connect.d.ts +76 -0
- package/dist/cli/connect.d.ts.map +1 -0
- package/dist/cli/connect.js +158 -0
- package/dist/cli/connect.js.map +1 -0
- package/dist/cli/doctor.d.ts +57 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +253 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +13 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +45 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +371 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -0
- package/dist/http/client-402.d.ts +243 -0
- package/dist/http/client-402.d.ts.map +1 -0
- package/dist/http/client-402.js +515 -0
- package/dist/http/client-402.js.map +1 -0
- package/dist/http/headers.d.ts +173 -0
- package/dist/http/headers.d.ts.map +1 -0
- package/dist/http/headers.js +284 -0
- package/dist/http/headers.js.map +1 -0
- package/dist/http/index.d.ts +15 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +15 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/metering-claim.d.ts +82 -0
- package/dist/http/metering-claim.d.ts.map +1 -0
- package/dist/http/metering-claim.js +99 -0
- package/dist/http/metering-claim.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +40 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +50 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/assets.d.ts +31 -0
- package/dist/mcp/assets.d.ts.map +1 -0
- package/dist/mcp/assets.js +78 -0
- package/dist/mcp/assets.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/json-schema.d.ts +86 -0
- package/dist/mcp/json-schema.d.ts.map +1 -0
- package/dist/mcp/json-schema.js +215 -0
- package/dist/mcp/json-schema.js.map +1 -0
- package/dist/mcp/json.d.ts +43 -0
- package/dist/mcp/json.d.ts.map +1 -0
- package/dist/mcp/json.js +69 -0
- package/dist/mcp/json.js.map +1 -0
- package/dist/mcp/registry-client.d.ts +88 -0
- package/dist/mcp/registry-client.d.ts.map +1 -0
- package/dist/mcp/registry-client.js +158 -0
- package/dist/mcp/registry-client.js.map +1 -0
- package/dist/mcp/schemas.d.ts +82 -0
- package/dist/mcp/schemas.d.ts.map +1 -0
- package/dist/mcp/schemas.js +493 -0
- package/dist/mcp/schemas.js.map +1 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +285 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/settings.d.ts +90 -0
- package/dist/mcp/settings.d.ts.map +1 -0
- package/dist/mcp/settings.js +160 -0
- package/dist/mcp/settings.js.map +1 -0
- package/dist/mcp/toolset.d.ts +231 -0
- package/dist/mcp/toolset.d.ts.map +1 -0
- package/dist/mcp/toolset.js +760 -0
- package/dist/mcp/toolset.js.map +1 -0
- package/dist/payments/abi.d.ts +9 -0
- package/dist/payments/abi.d.ts.map +1 -0
- package/dist/payments/abi.js +17 -0
- package/dist/payments/abi.js.map +1 -0
- package/dist/payments/config.d.ts +199 -0
- package/dist/payments/config.d.ts.map +1 -0
- package/dist/payments/config.js +259 -0
- package/dist/payments/config.js.map +1 -0
- package/dist/payments/index.d.ts +13 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +13 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/kuru.d.ts +191 -0
- package/dist/payments/kuru.d.ts.map +1 -0
- package/dist/payments/kuru.js +377 -0
- package/dist/payments/kuru.js.map +1 -0
- package/dist/payments/monad.d.ts +69 -0
- package/dist/payments/monad.d.ts.map +1 -0
- package/dist/payments/monad.js +306 -0
- package/dist/payments/monad.js.map +1 -0
- package/dist/payments/permit2.d.ts +118 -0
- package/dist/payments/permit2.d.ts.map +1 -0
- package/dist/payments/permit2.js +366 -0
- package/dist/payments/permit2.js.map +1 -0
- package/dist/payments/registry.d.ts +119 -0
- package/dist/payments/registry.d.ts.map +1 -0
- package/dist/payments/registry.js +199 -0
- package/dist/payments/registry.js.map +1 -0
- package/dist/payments/strategy.d.ts +80 -0
- package/dist/payments/strategy.d.ts.map +1 -0
- package/dist/payments/strategy.js +103 -0
- package/dist/payments/strategy.js.map +1 -0
- package/dist/proxy/hooks.d.ts +90 -0
- package/dist/proxy/hooks.d.ts.map +1 -0
- package/dist/proxy/hooks.js +35 -0
- package/dist/proxy/hooks.js.map +1 -0
- package/dist/proxy/index.d.ts +9 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +9 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/proxy.d.ts +156 -0
- package/dist/proxy/proxy.d.ts.map +1 -0
- package/dist/proxy/proxy.js +366 -0
- package/dist/proxy/proxy.js.map +1 -0
- package/dist/server/adapters/express.d.ts +89 -0
- package/dist/server/adapters/express.d.ts.map +1 -0
- package/dist/server/adapters/express.js +215 -0
- package/dist/server/adapters/express.js.map +1 -0
- package/dist/server/adapters/hono.d.ts +52 -0
- package/dist/server/adapters/hono.d.ts.map +1 -0
- package/dist/server/adapters/hono.js +61 -0
- package/dist/server/adapters/hono.js.map +1 -0
- package/dist/server/adapters/next.d.ts +52 -0
- package/dist/server/adapters/next.d.ts.map +1 -0
- package/dist/server/adapters/next.js +56 -0
- package/dist/server/adapters/next.js.map +1 -0
- package/dist/server/index.d.ts +30 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +30 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/metering.d.ts +209 -0
- package/dist/server/metering.d.ts.map +1 -0
- package/dist/server/metering.js +365 -0
- package/dist/server/metering.js.map +1 -0
- package/dist/server/post-paid.d.ts +355 -0
- package/dist/server/post-paid.d.ts.map +1 -0
- package/dist/server/post-paid.js +512 -0
- package/dist/server/post-paid.js.map +1 -0
- package/dist/x402/client.d.ts +203 -0
- package/dist/x402/client.d.ts.map +1 -0
- package/dist/x402/client.js +337 -0
- package/dist/x402/client.js.map +1 -0
- package/dist/x402/hub.d.ts +79 -0
- package/dist/x402/hub.d.ts.map +1 -0
- package/dist/x402/hub.js +164 -0
- package/dist/x402/hub.js.map +1 -0
- package/dist/x402/index.d.ts +27 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +27 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/proxy.d.ts +162 -0
- package/dist/x402/proxy.d.ts.map +1 -0
- package/dist/x402/proxy.js +198 -0
- package/dist/x402/proxy.js.map +1 -0
- package/dist/x402/server.d.ts +162 -0
- package/dist/x402/server.d.ts.map +1 -0
- package/dist/x402/server.js +306 -0
- package/dist/x402/server.js.map +1 -0
- package/dist/x402/wire.d.ts +104 -0
- package/dist/x402/wire.d.ts.map +1 -0
- package/dist/x402/wire.js +265 -0
- package/dist/x402/wire.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server-side post-paid plugin: it accrues, and it never gates.
|
|
3
|
+
*
|
|
4
|
+
* ## The inversion, stated once
|
|
5
|
+
*
|
|
6
|
+
* The handler runs. The handler produces its response. The response goes out.
|
|
7
|
+
* Only then is the delivered work metered into the Open Tab. Nothing in this file
|
|
8
|
+
* awaits a payment, a Settlement, or a signature before a response is
|
|
9
|
+
* released, because a plugin that did would be the prepay model this rail exists
|
|
10
|
+
* to replace. Metering is a record of work already delivered (R23.3, R12.1).
|
|
11
|
+
*
|
|
12
|
+
* Two orderings ship, and the difference is exactly one `await`:
|
|
13
|
+
*
|
|
14
|
+
* | `release` | what happens | can this request see a 402? |
|
|
15
|
+
* | --- | --- | --- |
|
|
16
|
+
* | `after-metering` (default) | handler resolves, the delivery is recorded, then the response is released carrying its charge headers | yes |
|
|
17
|
+
* | `before-metering` | handler resolves, the response is released, the delivery is recorded behind it | no, the response has already gone |
|
|
18
|
+
*
|
|
19
|
+
* `after-metering` is the default because a `LimitExceeded` refusal can only reach
|
|
20
|
+
* the caller on the request that caused it, and design section 9.5 asks for that
|
|
21
|
+
* refusal. What it holds the response for is a single credit record, not a
|
|
22
|
+
* payment, and it holds it for nothing else: if metering fails for any reason
|
|
23
|
+
* that is not the Agent's to fix, the handler's own response is delivered
|
|
24
|
+
* unchanged. A Service whose billing is broken pays for the delivery. It does not
|
|
25
|
+
* hand the cost to the caller.
|
|
26
|
+
*
|
|
27
|
+
* `before-metering` is there for a Service that will not accept even that much
|
|
28
|
+
* latency, and it is the shape that makes the guarantee observable: the response
|
|
29
|
+
* is returned while the metering promise is still pending.
|
|
30
|
+
*
|
|
31
|
+
* ## 402 is the exception, not the path
|
|
32
|
+
*
|
|
33
|
+
* The normal path is 200, delivery recorded, charge headers attached. `402`
|
|
34
|
+
* happens on `LimitExceeded` alone, the Agent's Open Tab for the Asset has no
|
|
35
|
+
* headroom for this charge, which is a credit decision and not a prepayment
|
|
36
|
+
* demand. When it happens the body carries the required amount and the current
|
|
37
|
+
* headroom, and the same figures go out as headers, so the 402 client of design
|
|
38
|
+
* section 9.4 reads a refusal with the identical parser it uses on a success.
|
|
39
|
+
*
|
|
40
|
+
* Two other refusals also reach the Agent, because only the Agent can clear them:
|
|
41
|
+
* a missing, lapsed, or spent spending authorisation (403), and a tab that went
|
|
42
|
+
* past its Settlement Window (409). Neither is a 402. Every other refusal in
|
|
43
|
+
* `metering.ts`'s table is the Service's to fix and never changes what the caller
|
|
44
|
+
* receives.
|
|
45
|
+
*
|
|
46
|
+
* ## Header contract
|
|
47
|
+
*
|
|
48
|
+
* The wire format is defined once, in `src/http/headers.ts`, and this plugin is
|
|
49
|
+
* its emitting half: every header it writes goes through `formatChargeHeaders`,
|
|
50
|
+
* which is the same module the 402 client parses with. So there is one definition
|
|
51
|
+
* of `Tab-Charge-Amount`, one of `Tab-Charge-Asset`, and no second string-building
|
|
52
|
+
* routine here to drift away from it. Three consequences are this side's to state:
|
|
53
|
+
*
|
|
54
|
+
* **The six charge headers are one all-or-nothing block.** `Tab-Charge-Amount`,
|
|
55
|
+
* `Tab-Charge-Asset`, `Tab-Charge-Service`, `Tab-Charge-Tool`, `Tab-Open-Tab`, and
|
|
56
|
+
* `Tab-Headroom` go out together or not at all, because a client cannot complete a
|
|
57
|
+
* partial block by guessing and a missing header is not a zero. A response with
|
|
58
|
+
* none of them is simply not a metered response.
|
|
59
|
+
*
|
|
60
|
+
* **A `402` carries the whole block, which is why the metering seam has a second
|
|
61
|
+
* method.** `LimitExceeded` reverts with the requested amount and the headroom and
|
|
62
|
+
* says nothing about the Open Tab, so the plugin reads it through
|
|
63
|
+
* `TabBookClient.openTabOf`, a view call, truthful precisely because the metering
|
|
64
|
+
* transaction reverted and moved nothing. Where that read fails, the refusal goes
|
|
65
|
+
* out with no charge headers rather than with an invented figure, and its body
|
|
66
|
+
* still names the required amount and the headroom.
|
|
67
|
+
*
|
|
68
|
+
* **A 403 or 409 refusal carries no charge block.** `AuthorisationMissing`,
|
|
69
|
+
* `AuthorisationExpired`, `AuthorisationExceeded`, and `TabIsDelinquent` revert
|
|
70
|
+
* with no headroom figure, so there is no complete block to send. The body carries
|
|
71
|
+
* the required amount, the reason, and the action.
|
|
72
|
+
*
|
|
73
|
+
* `Tab-Agent` and `Tab-Authorisation` are read from the request. Both are claims,
|
|
74
|
+
* and neither is an authentication. `TabBook` derives the authKey itself from
|
|
75
|
+
* `(agent, serviceId, asset)`, so `Tab-Authorisation` cannot redirect a charge: it
|
|
76
|
+
* is the Agent's statement about which authorisation it expects to be metered
|
|
77
|
+
* against, available on the charge for logging and carrying no authority. Metering
|
|
78
|
+
* is bounded by the on-chain authorisation the Agent set, and a Service that needs
|
|
79
|
+
* the `Tab-Agent` claim authenticated does that in `agentOf`.
|
|
80
|
+
*
|
|
81
|
+
* Requirements: 23.3, 12.1, 12.2, 12.3, 21.5
|
|
82
|
+
*/
|
|
83
|
+
import { isAddress, isBytes32 } from "../_shared/index.js";
|
|
84
|
+
import { defaultLogger } from "../logger.js";
|
|
85
|
+
import { tabError } from "../errors.js";
|
|
86
|
+
import { TAB_HEADER, chargedAssetKey, formatChargeHeaders, } from "../http/headers.js";
|
|
87
|
+
import { classifyRecordDeliveryRevert, detailAmount, dispositionOf, refusalStatusOf, } from "./metering.js";
|
|
88
|
+
/**
|
|
89
|
+
* Builds the plugin.
|
|
90
|
+
*
|
|
91
|
+
* Construction is total: it cannot fail and returns a {@link PostPaidPlugin}
|
|
92
|
+
* rather than a `Result`. Everything fallible belongs to the call that meters,
|
|
93
|
+
* because that is the only place a caller can do anything about it.
|
|
94
|
+
*/
|
|
95
|
+
export function tabPostPaid(options) {
|
|
96
|
+
const logger = options.logger ?? defaultLogger;
|
|
97
|
+
const now = options.now ?? (() => Date.now());
|
|
98
|
+
const release = options.release ?? "after-metering";
|
|
99
|
+
const { serviceId, asset, tabBook } = options;
|
|
100
|
+
const billable = options.billable ?? ((response) => response.status < 400);
|
|
101
|
+
const notMetered = (reason, detail) => ({
|
|
102
|
+
kind: "not-metered",
|
|
103
|
+
reason,
|
|
104
|
+
detail,
|
|
105
|
+
});
|
|
106
|
+
const failed = (error, context) => {
|
|
107
|
+
logger.warn("post-paid metering failed and the response was delivered anyway", {
|
|
108
|
+
serviceId,
|
|
109
|
+
code: error.code,
|
|
110
|
+
category: error.category,
|
|
111
|
+
message: error.message,
|
|
112
|
+
});
|
|
113
|
+
call(() => options.onMeteringFailed?.({ ...context, error }), "onMeteringFailed", logger);
|
|
114
|
+
return { kind: "failed", error };
|
|
115
|
+
};
|
|
116
|
+
const meter = async (request, response) => {
|
|
117
|
+
const deliverable = call(() => billable(response, request), "billable", logger);
|
|
118
|
+
if (!deliverable.ok) {
|
|
119
|
+
return failed(tabError("INTERNAL", "BILLABLE_THREW", "the billable predicate threw, so nothing was metered", {
|
|
120
|
+
cause: deliverable.cause,
|
|
121
|
+
}), { request });
|
|
122
|
+
}
|
|
123
|
+
if (deliverable.value !== true) {
|
|
124
|
+
return notMetered("not-billable", `status ${response.status} is not a delivery this Service charges for`);
|
|
125
|
+
}
|
|
126
|
+
const agent = resolveAgent(request, options.agentOf, logger);
|
|
127
|
+
if (agent.kind !== "ok")
|
|
128
|
+
return notMetered(agent.kind, agent.detail);
|
|
129
|
+
const quote = resolveQuote(request, options.priceOf, logger);
|
|
130
|
+
if (quote.kind !== "ok")
|
|
131
|
+
return notMetered(quote.kind, quote.detail);
|
|
132
|
+
const required = BigInt(quote.value.units) * quote.value.unitPrice;
|
|
133
|
+
const delivery = {
|
|
134
|
+
agent: agent.value,
|
|
135
|
+
serviceId,
|
|
136
|
+
asset,
|
|
137
|
+
tool: quote.value.tool,
|
|
138
|
+
units: quote.value.units,
|
|
139
|
+
expectedUnitPrice: quote.value.unitPrice,
|
|
140
|
+
};
|
|
141
|
+
const recorded = await callAsync(() => tabBook.recordDelivery(delivery), "tabBook.recordDelivery", logger);
|
|
142
|
+
if (!recorded.ok) {
|
|
143
|
+
// A `TabBookClient` is not supposed to throw. One that does is treated as a
|
|
144
|
+
// revert of unknown shape, which lands on `deliver-anyway`.
|
|
145
|
+
return failed(classifyRecordDeliveryRevert(recorded.thrown), {
|
|
146
|
+
request,
|
|
147
|
+
agent: agent.value,
|
|
148
|
+
quote: quote.value,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!recorded.value.ok) {
|
|
152
|
+
const error = recorded.value.error;
|
|
153
|
+
if (dispositionOf(error) !== "refuse-request") {
|
|
154
|
+
return failed(error, { request, agent: agent.value, quote: quote.value });
|
|
155
|
+
}
|
|
156
|
+
return refuse({
|
|
157
|
+
request,
|
|
158
|
+
agent: agent.value,
|
|
159
|
+
asset,
|
|
160
|
+
serviceId,
|
|
161
|
+
quote: quote.value,
|
|
162
|
+
required,
|
|
163
|
+
error,
|
|
164
|
+
// Read only where the refusal reports a headroom, because the charge block
|
|
165
|
+
// needs both figures or neither. `LimitExceeded` is that case.
|
|
166
|
+
openTab: detailAmount(error, "headroom") === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: await openTabFor(delivery),
|
|
169
|
+
logger,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const receipt = recorded.value.value;
|
|
173
|
+
const claim = claimOf(request);
|
|
174
|
+
const charge = {
|
|
175
|
+
agent: agent.value,
|
|
176
|
+
serviceId,
|
|
177
|
+
asset,
|
|
178
|
+
tool: quote.value.tool,
|
|
179
|
+
units: quote.value.units,
|
|
180
|
+
unitPrice: quote.value.unitPrice,
|
|
181
|
+
amount: receipt.charged,
|
|
182
|
+
openTabAfter: receipt.openAfter,
|
|
183
|
+
headroomAfter: receipt.headroomAfter,
|
|
184
|
+
...(claim === undefined ? {} : { authorisationClaim: claim }),
|
|
185
|
+
...(receipt.monadTxHash === undefined ? {} : { monadTxHash: receipt.monadTxHash }),
|
|
186
|
+
recordedAt: receipt.recordedAt,
|
|
187
|
+
};
|
|
188
|
+
call(() => options.onCharge?.(charge), "onCharge", logger);
|
|
189
|
+
return { kind: "charged", charge, receipt, headers: chargeHeaders(charge, logger) };
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The Open Tab the refusal reports, or undefined when it could not be read.
|
|
193
|
+
*
|
|
194
|
+
* A failed read costs the refusal its charge headers and nothing else: the body
|
|
195
|
+
* still names the required amount and the headroom, and no figure is invented to
|
|
196
|
+
* fill the block.
|
|
197
|
+
*/
|
|
198
|
+
const openTabFor = async (delivery) => {
|
|
199
|
+
const figure = await callAsync(() => tabBook.openTabOf(delivery), "tabBook.openTabOf", logger);
|
|
200
|
+
if (!figure.ok)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (!figure.value.ok) {
|
|
203
|
+
logger.warn("post-paid could not read the Open Tab, so the refusal carries no charge headers", {
|
|
204
|
+
serviceId,
|
|
205
|
+
code: figure.value.error.code,
|
|
206
|
+
});
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return figure.value.value;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* The response a refusal is served with.
|
|
213
|
+
*
|
|
214
|
+
* The `?? 0n` below is unreachable in practice: `LimitExceeded` reverts with a
|
|
215
|
+
* headroom, which is what put the figure on the context. It exists so
|
|
216
|
+
* {@link LimitExceededContext} can promise a `bigint` rather than making every
|
|
217
|
+
* consumer of the hook handle an absence that the revert set does not produce.
|
|
218
|
+
*/
|
|
219
|
+
const refusalResponseFor = (outcome) => {
|
|
220
|
+
const override = outcome.error.code === "LIMIT_EXCEEDED" && options.onLimitExceeded !== undefined
|
|
221
|
+
? call(() => options.onLimitExceeded?.({
|
|
222
|
+
...outcome.context,
|
|
223
|
+
headroomBaseUnits: outcome.context.headroomBaseUnits ?? 0n,
|
|
224
|
+
}), "onLimitExceeded", logger)
|
|
225
|
+
: options.onRefused !== undefined
|
|
226
|
+
? call(() => options.onRefused?.(outcome.context), "onRefused", logger)
|
|
227
|
+
: { ok: true, value: undefined };
|
|
228
|
+
if (override.ok && override.value !== undefined)
|
|
229
|
+
return override.value;
|
|
230
|
+
return jsonResponse(outcome.status, outcome.headers, outcome.body);
|
|
231
|
+
};
|
|
232
|
+
const plugin = {
|
|
233
|
+
serviceId,
|
|
234
|
+
asset,
|
|
235
|
+
release,
|
|
236
|
+
meter,
|
|
237
|
+
async execute(request, handler) {
|
|
238
|
+
// The handler runs first and runs to completion. Nothing above this line
|
|
239
|
+
// touches the chain, and nothing below it runs until the response exists.
|
|
240
|
+
let response;
|
|
241
|
+
try {
|
|
242
|
+
response = await handler();
|
|
243
|
+
}
|
|
244
|
+
catch (thrown) {
|
|
245
|
+
// No response means no delivery, and a delivery that did not happen is
|
|
246
|
+
// not charged for. Deliberate, and not configurable.
|
|
247
|
+
logger.debug("post-paid metered nothing because the handler failed", { serviceId });
|
|
248
|
+
return {
|
|
249
|
+
kind: "handler-failed",
|
|
250
|
+
thrown,
|
|
251
|
+
error: tabError("INTERNAL", "HANDLER_FAILED", "the handler failed, so no delivery was metered", {
|
|
252
|
+
details: { metered: false },
|
|
253
|
+
cause: causeOf(thrown),
|
|
254
|
+
}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (release === "before-metering") {
|
|
258
|
+
// The response is handed back now. The metering promise is still pending,
|
|
259
|
+
// and a caller with a host lifetime hook, `waitUntil`, `after`, passes
|
|
260
|
+
// it there so the runtime does not tear down underneath it.
|
|
261
|
+
return { kind: "delivered", response, metering: meter(request, response) };
|
|
262
|
+
}
|
|
263
|
+
const outcome = await meter(request, response);
|
|
264
|
+
if (outcome.kind === "refused") {
|
|
265
|
+
return { kind: "refused", response: refusalResponseFor(outcome), outcome };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
kind: "delivered",
|
|
269
|
+
response: attachHeaders(response, plugin.headersFor(outcome), logger),
|
|
270
|
+
metering: Promise.resolve(outcome),
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
headersFor(outcome) {
|
|
274
|
+
if (outcome.kind === "charged")
|
|
275
|
+
return outcome.headers;
|
|
276
|
+
if (outcome.kind === "refused")
|
|
277
|
+
return outcome.headers;
|
|
278
|
+
return {};
|
|
279
|
+
},
|
|
280
|
+
refusalResponseFor,
|
|
281
|
+
};
|
|
282
|
+
return plugin;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The six charge headers a recorded charge puts on the response.
|
|
286
|
+
*
|
|
287
|
+
* Formatted by `src/http/headers.ts`, which is also what the 402 client parses
|
|
288
|
+
* with, so the two halves of the wire format cannot disagree. A block that will
|
|
289
|
+
* not format is dropped rather than half-written, the client treats a partial
|
|
290
|
+
* block as an error, and correctly.
|
|
291
|
+
*/
|
|
292
|
+
export function chargeHeaders(charge, logger = defaultLogger) {
|
|
293
|
+
return blockHeaders({
|
|
294
|
+
amount: charge.amount,
|
|
295
|
+
asset: chargedAssetOf(charge.asset),
|
|
296
|
+
serviceId: charge.serviceId,
|
|
297
|
+
tool: charge.tool,
|
|
298
|
+
openTab: charge.openTabAfter,
|
|
299
|
+
headroom: charge.headroomAfter,
|
|
300
|
+
}, logger);
|
|
301
|
+
}
|
|
302
|
+
/** The Asset as the header contract carries it: a chain id and an address. */
|
|
303
|
+
const chargedAssetOf = (asset) => ({
|
|
304
|
+
chainId: asset.chainId,
|
|
305
|
+
address: asset.address,
|
|
306
|
+
});
|
|
307
|
+
/** Formats one charge block, or nothing at all if it would be malformed. */
|
|
308
|
+
function blockHeaders(block, logger) {
|
|
309
|
+
const formatted = formatChargeHeaders(block);
|
|
310
|
+
if (formatted.ok)
|
|
311
|
+
return formatted.value;
|
|
312
|
+
logger.error("post-paid built a charge block that will not format, so no charge headers were sent", {
|
|
313
|
+
code: formatted.error.code,
|
|
314
|
+
message: formatted.error.message,
|
|
315
|
+
});
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Builds the refusal: its status, its headers, and its body.
|
|
320
|
+
*
|
|
321
|
+
* The status comes from the error's category and from nothing else, which is why
|
|
322
|
+
* `LIMIT` is the only category that can produce a 402 here.
|
|
323
|
+
*
|
|
324
|
+
* The charge headers go out as the complete six-header block or not at all. That
|
|
325
|
+
* needs both a headroom, which only `LimitExceeded` reverts with, and an Open Tab,
|
|
326
|
+
* which no revert carries and which the caller read through `openTabOf`. Where
|
|
327
|
+
* either is missing the block is dropped, and the body, which always names the
|
|
328
|
+
* required amount, and names the headroom whenever the refusal reported one -
|
|
329
|
+
* carries the refusal on its own.
|
|
330
|
+
*/
|
|
331
|
+
function refuse(input) {
|
|
332
|
+
const { error, quote, required, openTab } = input;
|
|
333
|
+
const status = refusalStatusOf(error);
|
|
334
|
+
const headroom = detailAmount(error, "headroom");
|
|
335
|
+
const headers = headroom === undefined || openTab === undefined
|
|
336
|
+
? {}
|
|
337
|
+
: blockHeaders({
|
|
338
|
+
amount: required,
|
|
339
|
+
asset: chargedAssetOf(input.asset),
|
|
340
|
+
serviceId: input.serviceId,
|
|
341
|
+
tool: quote.tool,
|
|
342
|
+
openTab,
|
|
343
|
+
headroom,
|
|
344
|
+
}, input.logger);
|
|
345
|
+
const body = {
|
|
346
|
+
ok: false,
|
|
347
|
+
error: {
|
|
348
|
+
category: error.category,
|
|
349
|
+
code: error.code,
|
|
350
|
+
message: error.message,
|
|
351
|
+
retryable: error.retryable,
|
|
352
|
+
},
|
|
353
|
+
agent: input.agent,
|
|
354
|
+
serviceId: input.serviceId,
|
|
355
|
+
asset: chargedAssetKey(chargedAssetOf(input.asset)),
|
|
356
|
+
tool: quote.tool,
|
|
357
|
+
requiredBaseUnits: required.toString(10),
|
|
358
|
+
...(headroom === undefined ? {} : { headroomBaseUnits: headroom.toString(10) }),
|
|
359
|
+
action: actionOf(error),
|
|
360
|
+
};
|
|
361
|
+
const context = {
|
|
362
|
+
request: input.request,
|
|
363
|
+
agent: input.agent,
|
|
364
|
+
serviceId: input.serviceId,
|
|
365
|
+
asset: input.asset,
|
|
366
|
+
quote,
|
|
367
|
+
requiredBaseUnits: required,
|
|
368
|
+
...(headroom === undefined ? {} : { headroomBaseUnits: headroom }),
|
|
369
|
+
error,
|
|
370
|
+
status,
|
|
371
|
+
headers,
|
|
372
|
+
body,
|
|
373
|
+
};
|
|
374
|
+
return { kind: "refused", error, status, headers, body, context };
|
|
375
|
+
}
|
|
376
|
+
/** A JSON response carrying the refusal, with its `Tab-*` headers. */
|
|
377
|
+
export function jsonResponse(status, headers, body) {
|
|
378
|
+
return new Response(JSON.stringify(body), {
|
|
379
|
+
status,
|
|
380
|
+
headers: { ...headers, "Content-Type": "application/json; charset=utf-8" },
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Puts the charge headers on the delivered response.
|
|
385
|
+
*
|
|
386
|
+
* Mutation first, because that keeps the handler's own response object, and a
|
|
387
|
+
* test asserting the delivered response is the handler's response is asserting
|
|
388
|
+
* something worth keeping true. A `Response` that came back from `fetch` guards
|
|
389
|
+
* its headers as immutable, so the fallback rebuilds it around the same body
|
|
390
|
+
* stream rather than reading the body into memory.
|
|
391
|
+
*/
|
|
392
|
+
export function attachHeaders(response, headers, logger = defaultLogger) {
|
|
393
|
+
const entries = Object.entries(headers);
|
|
394
|
+
if (entries.length === 0)
|
|
395
|
+
return response;
|
|
396
|
+
try {
|
|
397
|
+
for (const [name, value] of entries)
|
|
398
|
+
response.headers.set(name, value);
|
|
399
|
+
return response;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
logger.debug("response headers were immutable, so the charge headers went onto a copy");
|
|
403
|
+
const merged = new Headers(response.headers);
|
|
404
|
+
for (const [name, value] of entries)
|
|
405
|
+
merged.set(name, value);
|
|
406
|
+
return new Response(response.body, {
|
|
407
|
+
status: response.status,
|
|
408
|
+
statusText: response.statusText,
|
|
409
|
+
headers: merged,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** The action text a refusal body carries, taken from the classified message. */
|
|
414
|
+
function actionOf(error) {
|
|
415
|
+
const separator = error.message.indexOf(": ");
|
|
416
|
+
return separator === -1 ? error.message : error.message.slice(separator + 2);
|
|
417
|
+
}
|
|
418
|
+
/** The `Tab-Authorisation` claim, when the Agent made one. */
|
|
419
|
+
const claimOf = (request) => {
|
|
420
|
+
const raw = read(request.headers, TAB_HEADER.authorisation);
|
|
421
|
+
return raw === undefined || raw.length === 0 ? undefined : raw;
|
|
422
|
+
};
|
|
423
|
+
/** Reads one header, tolerating a reader that throws or returns null. */
|
|
424
|
+
function read(headers, name) {
|
|
425
|
+
try {
|
|
426
|
+
const value = headers.get(name);
|
|
427
|
+
return typeof value === "string" ? value.trim() : undefined;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** The Agent to charge, from `agentOf` or from `Tab-Agent`. */
|
|
434
|
+
function resolveAgent(request, agentOf, logger) {
|
|
435
|
+
let raw;
|
|
436
|
+
if (agentOf !== undefined) {
|
|
437
|
+
const supplied = call(() => agentOf(request), "agentOf", logger);
|
|
438
|
+
if (!supplied.ok)
|
|
439
|
+
return { kind: "no-agent", detail: "agentOf threw" };
|
|
440
|
+
raw = supplied.value === null || supplied.value === undefined ? undefined : supplied.value.trim();
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
raw = read(request.headers, TAB_HEADER.agent);
|
|
444
|
+
}
|
|
445
|
+
if (raw === undefined || raw.length === 0) {
|
|
446
|
+
return {
|
|
447
|
+
kind: "no-agent",
|
|
448
|
+
detail: `no ${TAB_HEADER.agent} header, so there is no Agent to charge`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (!isAddress(raw)) {
|
|
452
|
+
return {
|
|
453
|
+
kind: "agent-malformed",
|
|
454
|
+
detail: `${TAB_HEADER.agent} carried \`${raw}\`, which is not a 20-byte 0x address`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return { kind: "ok", value: raw };
|
|
458
|
+
}
|
|
459
|
+
/** The price of this call, validated into something `recordDelivery` accepts. */
|
|
460
|
+
function resolveQuote(request, priceOf, logger) {
|
|
461
|
+
const quoted = call(() => priceOf(request), "priceOf", logger);
|
|
462
|
+
if (!quoted.ok)
|
|
463
|
+
return { kind: "not-priced", detail: "priceOf threw" };
|
|
464
|
+
const quote = quoted.value;
|
|
465
|
+
if (quote === undefined || quote === null) {
|
|
466
|
+
return { kind: "not-priced", detail: "priceOf returned nothing, so this request is not charged for" };
|
|
467
|
+
}
|
|
468
|
+
if (!isBytes32(quote.tool)) {
|
|
469
|
+
return { kind: "price-invalid", detail: "quote.tool must be a 32-byte 0x word" };
|
|
470
|
+
}
|
|
471
|
+
if (!Number.isInteger(quote.units) || quote.units <= 0 || quote.units > 4_294_967_295) {
|
|
472
|
+
return {
|
|
473
|
+
kind: "price-invalid",
|
|
474
|
+
detail: `quote.units must be a positive integer inside uint32, received ${String(quote.units)}`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (typeof quote.unitPrice !== "bigint" || quote.unitPrice <= 0n) {
|
|
478
|
+
return {
|
|
479
|
+
kind: "price-invalid",
|
|
480
|
+
detail: "quote.unitPrice must be a positive bigint count of Asset base units, never a number",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return { kind: "ok", value: quote };
|
|
484
|
+
}
|
|
485
|
+
/** Runs consumer code without letting it throw into the metering path. */
|
|
486
|
+
function call(fn, what, logger) {
|
|
487
|
+
try {
|
|
488
|
+
return { ok: true, value: fn() };
|
|
489
|
+
}
|
|
490
|
+
catch (thrown) {
|
|
491
|
+
const cause = causeOf(thrown);
|
|
492
|
+
logger.error(`post-paid ${what} threw and was ignored`, cause);
|
|
493
|
+
return { ok: false, cause };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** The asynchronous sibling, keeping a rejected promise out of the metering path. */
|
|
497
|
+
async function callAsync(fn, what, logger) {
|
|
498
|
+
try {
|
|
499
|
+
return { ok: true, value: await fn() };
|
|
500
|
+
}
|
|
501
|
+
catch (thrown) {
|
|
502
|
+
logger.error(`post-paid ${what} threw and was treated as a refusal of unknown shape`, causeOf(thrown));
|
|
503
|
+
return { ok: false, thrown };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/** Narrows a caught value into the `cause` shape a `TabError` accepts. */
|
|
507
|
+
function causeOf(thrown) {
|
|
508
|
+
if (thrown instanceof Error)
|
|
509
|
+
return { code: thrown.name, message: thrown.message };
|
|
510
|
+
return { code: "UNKNOWN", message: String(thrown) };
|
|
511
|
+
}
|
|
512
|
+
//# sourceMappingURL=post-paid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-paid.js","sourceRoot":"","sources":["../../src/server/post-paid.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The server-side post-paid plugin: it accrues, and it never gates.\n *\n * ## The inversion, stated once\n *\n * The handler runs. The handler produces its response. The response goes out.\n * Only then is the delivered work metered into the Open Tab. Nothing in this file\n * awaits a payment, a Settlement, or a signature before a response is\n * released, because a plugin that did would be the prepay model this rail exists\n * to replace. Metering is a record of work already delivered (R23.3, R12.1).\n *\n * Two orderings ship, and the difference is exactly one `await`:\n *\n * | `release` | what happens | can this request see a 402? |\n * | --- | --- | --- |\n * | `after-metering` (default) | handler resolves, the delivery is recorded, then the response is released carrying its charge headers | yes |\n * | `before-metering` | handler resolves, the response is released, the delivery is recorded behind it | no, the response has already gone |\n *\n * `after-metering` is the default because a `LimitExceeded` refusal can only reach\n * the caller on the request that caused it, and design section 9.5 asks for that\n * refusal. What it holds the response for is a single credit record, not a\n * payment, and it holds it for nothing else: if metering fails for any reason\n * that is not the Agent's to fix, the handler's own response is delivered\n * unchanged. A Service whose billing is broken pays for the delivery. It does not\n * hand the cost to the caller.\n *\n * `before-metering` is there for a Service that will not accept even that much\n * latency, and it is the shape that makes the guarantee observable: the response\n * is returned while the metering promise is still pending.\n *\n * ## 402 is the exception, not the path\n *\n * The normal path is 200, delivery recorded, charge headers attached. `402`\n * happens on `LimitExceeded` alone, the Agent's Open Tab for the Asset has no\n * headroom for this charge, which is a credit decision and not a prepayment\n * demand. When it happens the body carries the required amount and the current\n * headroom, and the same figures go out as headers, so the 402 client of design\n * section 9.4 reads a refusal with the identical parser it uses on a success.\n *\n * Two other refusals also reach the Agent, because only the Agent can clear them:\n * a missing, lapsed, or spent spending authorisation (403), and a tab that went\n * past its Settlement Window (409). Neither is a 402. Every other refusal in\n * `metering.ts`'s table is the Service's to fix and never changes what the caller\n * receives.\n *\n * ## Header contract\n *\n * The wire format is defined once, in `src/http/headers.ts`, and this plugin is\n * its emitting half: every header it writes goes through `formatChargeHeaders`,\n * which is the same module the 402 client parses with. So there is one definition\n * of `Tab-Charge-Amount`, one of `Tab-Charge-Asset`, and no second string-building\n * routine here to drift away from it. Three consequences are this side's to state:\n *\n * **The six charge headers are one all-or-nothing block.** `Tab-Charge-Amount`,\n * `Tab-Charge-Asset`, `Tab-Charge-Service`, `Tab-Charge-Tool`, `Tab-Open-Tab`, and\n * `Tab-Headroom` go out together or not at all, because a client cannot complete a\n * partial block by guessing and a missing header is not a zero. A response with\n * none of them is simply not a metered response.\n *\n * **A `402` carries the whole block, which is why the metering seam has a second\n * method.** `LimitExceeded` reverts with the requested amount and the headroom and\n * says nothing about the Open Tab, so the plugin reads it through\n * `TabBookClient.openTabOf`, a view call, truthful precisely because the metering\n * transaction reverted and moved nothing. Where that read fails, the refusal goes\n * out with no charge headers rather than with an invented figure, and its body\n * still names the required amount and the headroom.\n *\n * **A 403 or 409 refusal carries no charge block.** `AuthorisationMissing`,\n * `AuthorisationExpired`, `AuthorisationExceeded`, and `TabIsDelinquent` revert\n * with no headroom figure, so there is no complete block to send. The body carries\n * the required amount, the reason, and the action.\n *\n * `Tab-Agent` and `Tab-Authorisation` are read from the request. Both are claims,\n * and neither is an authentication. `TabBook` derives the authKey itself from\n * `(agent, serviceId, asset)`, so `Tab-Authorisation` cannot redirect a charge: it\n * is the Agent's statement about which authorisation it expects to be metered\n * against, available on the charge for logging and carrying no authority. Metering\n * is bounded by the on-chain authorisation the Agent set, and a Service that needs\n * the `Tab-Agent` claim authenticated does that in `agentOf`.\n *\n * Requirements: 23.3, 12.1, 12.2, 12.3, 21.5\n */\n\nimport { isAddress, isBytes32, type Address, type Bytes32, type Hex, type TabError } from \"../_shared/index.js\";\nimport { defaultLogger, type Logger } from \"../logger.js\";\nimport { tabError } from \"../errors.js\";\nimport type { AssetRef } from \"../payments/strategy.js\";\nimport {\n TAB_HEADER,\n chargedAssetKey,\n formatChargeHeaders,\n type ChargeBlock,\n type ChargedAsset,\n type HeaderReader,\n} from \"../http/headers.js\";\nimport {\n classifyRecordDeliveryRevert,\n detailAmount,\n dispositionOf,\n refusalStatusOf,\n type DeliveryReceipt,\n type MeteredDelivery,\n type TabBookClient,\n} from \"./metering.js\";\n\n/**\n * The request, reduced to what pricing and Agent identification need.\n *\n * Structural, and the body is not part of it. A web-standard `Request` satisfies\n * this as it stands, and the Express adapter builds one from `req` without\n * touching the stream, because buffering a request body to price it would take\n * the body away from the framework that owns it.\n */\nexport interface MeteredRequest {\n readonly method: string;\n readonly url: string;\n readonly headers: HeaderReader;\n}\n\n/**\n * The response, reduced to what the billability decision needs.\n *\n * A web-standard `Response` satisfies it, and so does an Express `res`, which is\n * why the metering core never mentions `Response` at all.\n */\nexport interface DeliveredResponse {\n readonly status: number;\n}\n\n/**\n * What the Service charges for this call.\n *\n * A deliberate superset of design section 9.5's `{ tool, units }`.\n * `recordDelivery` takes an `expectedUnitPrice` and reverts\n * `PriceListChangedMidCall` when it disagrees with the applied price list, so a\n * plugin that cannot state the price it quoted cannot call the contract at all.\n * The field is required rather than optional for the same reason: there is no\n * sensible default, and a zero would be read as a free tool.\n */\nexport interface PriceQuote {\n /** The named priced unit, as the 32-byte word the applied price list is keyed by. */\n readonly tool: Bytes32;\n /** Count of priced units. A positive integer inside `uint32`. */\n readonly units: number;\n /** Base units per unit of `tool`, exactly as quoted to the Agent. */\n readonly unitPrice: bigint;\n}\n\n/** One recorded charge, as the Service's own logging and hooks see it. */\nexport interface MeteredCharge {\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: AssetRef;\n /**\n * The 32-byte word the applied price list is keyed by, and what\n * `Tab-Charge-Tool` carries. The word rather than a decoded name, because it is\n * what round-trips back into a contract call unchanged.\n */\n readonly tool: Bytes32;\n readonly units: number;\n readonly unitPrice: bigint;\n /** `units * unitPrice`, as the contract computed it. */\n readonly amount: bigint;\n readonly openTabAfter: bigint;\n readonly headroomAfter: bigint;\n /** Whatever the Agent claimed in `Tab-Authorisation`, when it claimed anything. */\n readonly authorisationClaim?: string;\n readonly monadTxHash?: Hex;\n readonly recordedAt: number;\n}\n\n/** Why a request was not metered. Never an error: none of these is a failure. */\nexport type NotMeteredReason =\n /** No `Tab-Agent` header and no `agentOf`, so there is nobody to charge. */\n | \"no-agent\"\n /** `Tab-Agent` was present and was not a 20-byte address. */\n | \"agent-malformed\"\n /** `priceOf` returned nothing: the Service does not charge for this request. */\n | \"not-priced\"\n /** `priceOf` returned a quote that cannot be metered, such as zero units. */\n | \"price-invalid\"\n /** `billable` said this response is not a delivery, by default, any status at or above 400. */\n | \"not-billable\"\n /** The handler threw, so no delivery happened and nothing is charged for. */\n | \"handler-failed\";\n\nexport interface ChargedOutcome {\n readonly kind: \"charged\";\n readonly charge: MeteredCharge;\n readonly receipt: DeliveryReceipt;\n /** The `Tab-*` headers this charge puts on the response. */\n readonly headers: Readonly<Record<string, string>>;\n}\n\nexport interface NotMeteredOutcome {\n readonly kind: \"not-metered\";\n readonly reason: NotMeteredReason;\n readonly detail: string;\n}\n\n/** The body of a refusal. Amounts are decimal integer base-unit strings. */\nexport interface RefusalBody {\n readonly ok: false;\n readonly error: {\n readonly category: string;\n readonly code: string;\n readonly message: string;\n readonly retryable: boolean;\n };\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: string;\n readonly tool: string;\n /** Base units this call needed. Present on every refusal: it is the charge that was refused. */\n readonly requiredBaseUnits: string;\n /** Base units of headroom left. Present when the refusal reported one. */\n readonly headroomBaseUnits?: string;\n /** What the Agent has to do about it. */\n readonly action: string;\n}\n\n/** A refusal the Agent has to clear. The response is replaced by it. */\nexport interface RefusedOutcome {\n readonly kind: \"refused\";\n readonly error: TabError;\n readonly status: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly body: RefusalBody;\n readonly context: RefusalContext;\n}\n\n/** Metering failed and the response is delivered anyway. */\nexport interface MeteringFailedOutcome {\n readonly kind: \"failed\";\n readonly error: TabError;\n}\n\nexport type MeteringOutcome =\n | ChargedOutcome\n | NotMeteredOutcome\n | RefusedOutcome\n | MeteringFailedOutcome;\n\n/** Everything a refusal handler needs to write its own response. */\nexport interface RefusalContext {\n readonly request: MeteredRequest;\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: AssetRef;\n readonly quote: PriceQuote;\n /** Base units this call needed: `units * unitPrice`. */\n readonly requiredBaseUnits: bigint;\n /** Base units of headroom the refusal reported, when it reported one. */\n readonly headroomBaseUnits?: bigint;\n readonly error: TabError;\n readonly status: number;\n readonly headers: Readonly<Record<string, string>>;\n readonly body: RefusalBody;\n}\n\n/** A `LimitExceeded` refusal, where the headroom is always known. */\nexport interface LimitExceededContext extends RefusalContext {\n readonly headroomBaseUnits: bigint;\n}\n\n/** A metering failure the caller never sees. The Service does. */\nexport interface MeteringFailureContext {\n readonly request: MeteredRequest;\n readonly error: TabError;\n readonly agent?: Address;\n readonly quote?: PriceQuote;\n}\n\n/** When the delivered response is released relative to the metering call. */\nexport type ReleaseOrder = \"after-metering\" | \"before-metering\";\n\nexport interface TabPostPaidOptions {\n readonly serviceId: Bytes32;\n readonly asset: AssetRef;\n /**\n * What this request costs, or nothing when the Service does not charge for it.\n *\n * Called once, after the handler has produced its response, with the method,\n * URL, and headers of the request. Consumer code, so a throw is caught and read\n * as \"not priced\" rather than allowed to take down a delivered response.\n */\n readonly priceOf: (request: MeteredRequest) => PriceQuote | undefined | null;\n readonly tabBook: TabBookClient;\n /** Defaults to `after-metering`, the only ordering in which a 402 can reach this request. */\n readonly release?: ReleaseOrder;\n /**\n * Identifies the Agent. Defaults to the `Tab-Agent` request header.\n *\n * Supply this to authenticate the claim, a signature, a session, an API key\n * mapped to an address. The plugin does not authenticate it, and a Service\n * that needs it authenticated does it here.\n */\n readonly agentOf?: (request: MeteredRequest) => string | undefined | null;\n /**\n * Whether a response counts as a delivery worth charging for. Defaults to\n * `status < 400`.\n *\n * The policy this default states: an Agent is not charged for a response that\n * failed. A 5xx is the Service's fault and a 4xx delivered nothing the Agent\n * asked for. A Service that meters attempts rather than deliveries, a rate\n * limiter, say, overrides it.\n */\n readonly billable?: (response: DeliveredResponse, request: MeteredRequest) => boolean;\n readonly onCharge?: (charge: MeteredCharge) => void;\n /** Replaces the default 402. Called only for `LimitExceeded`. */\n readonly onLimitExceeded?: (context: LimitExceededContext) => Response;\n /** Replaces the default response for any other refusal the Agent must clear. */\n readonly onRefused?: (context: RefusalContext) => Response | undefined;\n /** Where a metering failure the caller never sees is reported. */\n readonly onMeteringFailed?: (context: MeteringFailureContext) => void;\n readonly logger?: Logger;\n /** Injectable clock, so `recordedAt` is testable. Defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/**\n * What `execute` did.\n *\n * `delivered` is the ordinary outcome and covers every case in which the\n * handler's own response is what goes out: charged, not metered, and metering\n * failed. `metering` resolves when the recording finished, which under\n * `before-metering` is after the response was already returned.\n */\nexport type PostPaidExecution =\n | {\n readonly kind: \"delivered\";\n readonly response: Response;\n readonly metering: Promise<MeteringOutcome>;\n }\n | {\n readonly kind: \"refused\";\n readonly response: Response;\n readonly outcome: RefusedOutcome;\n }\n | {\n readonly kind: \"handler-failed\";\n readonly thrown: unknown;\n readonly error: TabError;\n };\n\n/** The framework-free plugin the three adapters are thin wrappers over. */\nexport interface PostPaidPlugin {\n readonly serviceId: Bytes32;\n readonly asset: AssetRef;\n readonly release: ReleaseOrder;\n /**\n * Records the delivery for a response the caller already holds.\n *\n * Never throws and never rejects, whatever a consumer callback or a\n * {@link TabBookClient} does.\n */\n meter(request: MeteredRequest, response: DeliveredResponse): Promise<MeteringOutcome>;\n /**\n * Runs the handler, then meters. The handler is awaited to completion before\n * any metering call is made, always.\n */\n execute(request: MeteredRequest, handler: () => Response | Promise<Response>): Promise<PostPaidExecution>;\n /** The `Tab-*` headers an outcome puts on a response. Empty when there are none. */\n headersFor(outcome: MeteringOutcome): Readonly<Record<string, string>>;\n /** The response a refusal is served with, through the consumer's override when there is one. */\n refusalResponseFor(outcome: RefusedOutcome): Response;\n}\n\n/**\n * Builds the plugin.\n *\n * Construction is total: it cannot fail and returns a {@link PostPaidPlugin}\n * rather than a `Result`. Everything fallible belongs to the call that meters,\n * because that is the only place a caller can do anything about it.\n */\nexport function tabPostPaid(options: TabPostPaidOptions): PostPaidPlugin {\n const logger = options.logger ?? defaultLogger;\n const now = options.now ?? (() => Date.now());\n const release = options.release ?? \"after-metering\";\n const { serviceId, asset, tabBook } = options;\n const billable = options.billable ?? ((response: DeliveredResponse) => response.status < 400);\n\n const notMetered = (reason: NotMeteredReason, detail: string): NotMeteredOutcome => ({\n kind: \"not-metered\",\n reason,\n detail,\n });\n\n const failed = (error: TabError, context: Omit<MeteringFailureContext, \"error\">): MeteringFailedOutcome => {\n logger.warn(\"post-paid metering failed and the response was delivered anyway\", {\n serviceId,\n code: error.code,\n category: error.category,\n message: error.message,\n });\n call(() => options.onMeteringFailed?.({ ...context, error }), \"onMeteringFailed\", logger);\n return { kind: \"failed\", error };\n };\n\n const meter = async (\n request: MeteredRequest,\n response: DeliveredResponse,\n ): Promise<MeteringOutcome> => {\n const deliverable = call(() => billable(response, request), \"billable\", logger);\n if (!deliverable.ok) {\n return failed(\n tabError(\"INTERNAL\", \"BILLABLE_THREW\", \"the billable predicate threw, so nothing was metered\", {\n cause: deliverable.cause,\n }),\n { request },\n );\n }\n if (deliverable.value !== true) {\n return notMetered(\n \"not-billable\",\n `status ${response.status} is not a delivery this Service charges for`,\n );\n }\n\n const agent = resolveAgent(request, options.agentOf, logger);\n if (agent.kind !== \"ok\") return notMetered(agent.kind, agent.detail);\n\n const quote = resolveQuote(request, options.priceOf, logger);\n if (quote.kind !== \"ok\") return notMetered(quote.kind, quote.detail);\n\n const required = BigInt(quote.value.units) * quote.value.unitPrice;\n const delivery: MeteredDelivery = {\n agent: agent.value,\n serviceId,\n asset,\n tool: quote.value.tool,\n units: quote.value.units,\n expectedUnitPrice: quote.value.unitPrice,\n };\n const recorded = await callAsync(\n () => tabBook.recordDelivery(delivery),\n \"tabBook.recordDelivery\",\n logger,\n );\n\n if (!recorded.ok) {\n // A `TabBookClient` is not supposed to throw. One that does is treated as a\n // revert of unknown shape, which lands on `deliver-anyway`.\n return failed(classifyRecordDeliveryRevert(recorded.thrown), {\n request,\n agent: agent.value,\n quote: quote.value,\n });\n }\n\n if (!recorded.value.ok) {\n const error = recorded.value.error;\n if (dispositionOf(error) !== \"refuse-request\") {\n return failed(error, { request, agent: agent.value, quote: quote.value });\n }\n return refuse({\n request,\n agent: agent.value,\n asset,\n serviceId,\n quote: quote.value,\n required,\n error,\n // Read only where the refusal reports a headroom, because the charge block\n // needs both figures or neither. `LimitExceeded` is that case.\n openTab:\n detailAmount(error, \"headroom\") === undefined\n ? undefined\n : await openTabFor(delivery),\n logger,\n });\n }\n\n const receipt = recorded.value.value;\n const claim = claimOf(request);\n const charge: MeteredCharge = {\n agent: agent.value,\n serviceId,\n asset,\n tool: quote.value.tool,\n units: quote.value.units,\n unitPrice: quote.value.unitPrice,\n amount: receipt.charged,\n openTabAfter: receipt.openAfter,\n headroomAfter: receipt.headroomAfter,\n ...(claim === undefined ? {} : { authorisationClaim: claim }),\n ...(receipt.monadTxHash === undefined ? {} : { monadTxHash: receipt.monadTxHash }),\n recordedAt: receipt.recordedAt,\n };\n\n call(() => options.onCharge?.(charge), \"onCharge\", logger);\n\n return { kind: \"charged\", charge, receipt, headers: chargeHeaders(charge, logger) };\n };\n\n /**\n * The Open Tab the refusal reports, or undefined when it could not be read.\n *\n * A failed read costs the refusal its charge headers and nothing else: the body\n * still names the required amount and the headroom, and no figure is invented to\n * fill the block.\n */\n const openTabFor = async (delivery: MeteredDelivery): Promise<bigint | undefined> => {\n const figure = await callAsync(() => tabBook.openTabOf(delivery), \"tabBook.openTabOf\", logger);\n if (!figure.ok) return undefined;\n if (!figure.value.ok) {\n logger.warn(\"post-paid could not read the Open Tab, so the refusal carries no charge headers\", {\n serviceId,\n code: figure.value.error.code,\n });\n return undefined;\n }\n return figure.value.value;\n };\n\n /**\n * The response a refusal is served with.\n *\n * The `?? 0n` below is unreachable in practice: `LimitExceeded` reverts with a\n * headroom, which is what put the figure on the context. It exists so\n * {@link LimitExceededContext} can promise a `bigint` rather than making every\n * consumer of the hook handle an absence that the revert set does not produce.\n */\n const refusalResponseFor = (outcome: RefusedOutcome): Response => {\n const override =\n outcome.error.code === \"LIMIT_EXCEEDED\" && options.onLimitExceeded !== undefined\n ? call(\n () =>\n options.onLimitExceeded?.({\n ...outcome.context,\n headroomBaseUnits: outcome.context.headroomBaseUnits ?? 0n,\n }),\n \"onLimitExceeded\",\n logger,\n )\n : options.onRefused !== undefined\n ? call(() => options.onRefused?.(outcome.context), \"onRefused\", logger)\n : { ok: true as const, value: undefined };\n\n if (override.ok && override.value !== undefined) return override.value;\n return jsonResponse(outcome.status, outcome.headers, outcome.body);\n };\n\n const plugin: PostPaidPlugin = {\n serviceId,\n asset,\n release,\n\n meter,\n\n async execute(request, handler) {\n // The handler runs first and runs to completion. Nothing above this line\n // touches the chain, and nothing below it runs until the response exists.\n let response: Response;\n try {\n response = await handler();\n } catch (thrown) {\n // No response means no delivery, and a delivery that did not happen is\n // not charged for. Deliberate, and not configurable.\n logger.debug(\"post-paid metered nothing because the handler failed\", { serviceId });\n return {\n kind: \"handler-failed\",\n thrown,\n error: tabError(\"INTERNAL\", \"HANDLER_FAILED\", \"the handler failed, so no delivery was metered\", {\n details: { metered: false },\n cause: causeOf(thrown),\n }),\n };\n }\n\n if (release === \"before-metering\") {\n // The response is handed back now. The metering promise is still pending,\n // and a caller with a host lifetime hook, `waitUntil`, `after`, passes\n // it there so the runtime does not tear down underneath it.\n return { kind: \"delivered\", response, metering: meter(request, response) };\n }\n\n const outcome = await meter(request, response);\n if (outcome.kind === \"refused\") {\n return { kind: \"refused\", response: refusalResponseFor(outcome), outcome };\n }\n return {\n kind: \"delivered\",\n response: attachHeaders(response, plugin.headersFor(outcome), logger),\n metering: Promise.resolve(outcome),\n };\n },\n\n headersFor(outcome) {\n if (outcome.kind === \"charged\") return outcome.headers;\n if (outcome.kind === \"refused\") return outcome.headers;\n return {};\n },\n\n refusalResponseFor,\n };\n\n return plugin;\n}\n\n/**\n * The six charge headers a recorded charge puts on the response.\n *\n * Formatted by `src/http/headers.ts`, which is also what the 402 client parses\n * with, so the two halves of the wire format cannot disagree. A block that will\n * not format is dropped rather than half-written, the client treats a partial\n * block as an error, and correctly.\n */\nexport function chargeHeaders(\n charge: MeteredCharge,\n logger: Logger = defaultLogger,\n): Readonly<Record<string, string>> {\n return blockHeaders(\n {\n amount: charge.amount,\n asset: chargedAssetOf(charge.asset),\n serviceId: charge.serviceId,\n tool: charge.tool,\n openTab: charge.openTabAfter,\n headroom: charge.headroomAfter,\n },\n logger,\n );\n}\n\n/** The Asset as the header contract carries it: a chain id and an address. */\nconst chargedAssetOf = (asset: AssetRef): ChargedAsset => ({\n chainId: asset.chainId,\n address: asset.address,\n});\n\n/** Formats one charge block, or nothing at all if it would be malformed. */\nfunction blockHeaders(block: ChargeBlock, logger: Logger): Readonly<Record<string, string>> {\n const formatted = formatChargeHeaders(block);\n if (formatted.ok) return formatted.value;\n logger.error(\"post-paid built a charge block that will not format, so no charge headers were sent\", {\n code: formatted.error.code,\n message: formatted.error.message,\n });\n return {};\n}\n\n/**\n * Builds the refusal: its status, its headers, and its body.\n *\n * The status comes from the error's category and from nothing else, which is why\n * `LIMIT` is the only category that can produce a 402 here.\n *\n * The charge headers go out as the complete six-header block or not at all. That\n * needs both a headroom, which only `LimitExceeded` reverts with, and an Open Tab,\n * which no revert carries and which the caller read through `openTabOf`. Where\n * either is missing the block is dropped, and the body, which always names the\n * required amount, and names the headroom whenever the refusal reported one -\n * carries the refusal on its own.\n */\nfunction refuse(input: {\n readonly request: MeteredRequest;\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: AssetRef;\n readonly quote: PriceQuote;\n readonly required: bigint;\n readonly error: TabError;\n /** The Open Tab as read on the refusal path, absent when it could not be read. */\n readonly openTab: bigint | undefined;\n readonly logger: Logger;\n}): RefusedOutcome {\n const { error, quote, required, openTab } = input;\n const status = refusalStatusOf(error);\n const headroom = detailAmount(error, \"headroom\");\n\n const headers =\n headroom === undefined || openTab === undefined\n ? {}\n : blockHeaders(\n {\n amount: required,\n asset: chargedAssetOf(input.asset),\n serviceId: input.serviceId,\n tool: quote.tool,\n openTab,\n headroom,\n },\n input.logger,\n );\n\n const body: RefusalBody = {\n ok: false,\n error: {\n category: error.category,\n code: error.code,\n message: error.message,\n retryable: error.retryable,\n },\n agent: input.agent,\n serviceId: input.serviceId,\n asset: chargedAssetKey(chargedAssetOf(input.asset)),\n tool: quote.tool,\n requiredBaseUnits: required.toString(10),\n ...(headroom === undefined ? {} : { headroomBaseUnits: headroom.toString(10) }),\n action: actionOf(error),\n };\n\n const context: RefusalContext = {\n request: input.request,\n agent: input.agent,\n serviceId: input.serviceId,\n asset: input.asset,\n quote,\n requiredBaseUnits: required,\n ...(headroom === undefined ? {} : { headroomBaseUnits: headroom }),\n error,\n status,\n headers,\n body,\n };\n\n return { kind: \"refused\", error, status, headers, body, context };\n}\n\n/** A JSON response carrying the refusal, with its `Tab-*` headers. */\nexport function jsonResponse(\n status: number,\n headers: Readonly<Record<string, string>>,\n body: unknown,\n): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { ...headers, \"Content-Type\": \"application/json; charset=utf-8\" },\n });\n}\n\n/**\n * Puts the charge headers on the delivered response.\n *\n * Mutation first, because that keeps the handler's own response object, and a\n * test asserting the delivered response is the handler's response is asserting\n * something worth keeping true. A `Response` that came back from `fetch` guards\n * its headers as immutable, so the fallback rebuilds it around the same body\n * stream rather than reading the body into memory.\n */\nexport function attachHeaders(\n response: Response,\n headers: Readonly<Record<string, string>>,\n logger: Logger = defaultLogger,\n): Response {\n const entries = Object.entries(headers);\n if (entries.length === 0) return response;\n try {\n for (const [name, value] of entries) response.headers.set(name, value);\n return response;\n } catch {\n logger.debug(\"response headers were immutable, so the charge headers went onto a copy\");\n const merged = new Headers(response.headers);\n for (const [name, value] of entries) merged.set(name, value);\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: merged,\n });\n }\n}\n\n/** The action text a refusal body carries, taken from the classified message. */\nfunction actionOf(error: TabError): string {\n const separator = error.message.indexOf(\": \");\n return separator === -1 ? error.message : error.message.slice(separator + 2);\n}\n\n/** The `Tab-Authorisation` claim, when the Agent made one. */\nconst claimOf = (request: MeteredRequest): string | undefined => {\n const raw = read(request.headers, TAB_HEADER.authorisation);\n return raw === undefined || raw.length === 0 ? undefined : raw;\n};\n\n/** Reads one header, tolerating a reader that throws or returns null. */\nfunction read(headers: HeaderReader, name: string): string | undefined {\n try {\n const value = headers.get(name);\n return typeof value === \"string\" ? value.trim() : undefined;\n } catch {\n return undefined;\n }\n}\n\ntype Resolved<T> = { kind: \"ok\"; value: T } | { kind: NotMeteredReason; detail: string };\n\n/** The Agent to charge, from `agentOf` or from `Tab-Agent`. */\nfunction resolveAgent(\n request: MeteredRequest,\n agentOf: TabPostPaidOptions[\"agentOf\"],\n logger: Logger,\n): Resolved<Address> {\n let raw: string | undefined;\n if (agentOf !== undefined) {\n const supplied = call(() => agentOf(request), \"agentOf\", logger);\n if (!supplied.ok) return { kind: \"no-agent\", detail: \"agentOf threw\" };\n raw = supplied.value === null || supplied.value === undefined ? undefined : supplied.value.trim();\n } else {\n raw = read(request.headers, TAB_HEADER.agent);\n }\n\n if (raw === undefined || raw.length === 0) {\n return {\n kind: \"no-agent\",\n detail: `no ${TAB_HEADER.agent} header, so there is no Agent to charge`,\n };\n }\n if (!isAddress(raw)) {\n return {\n kind: \"agent-malformed\",\n detail: `${TAB_HEADER.agent} carried \\`${raw}\\`, which is not a 20-byte 0x address`,\n };\n }\n return { kind: \"ok\", value: raw };\n}\n\n/** The price of this call, validated into something `recordDelivery` accepts. */\nfunction resolveQuote(\n request: MeteredRequest,\n priceOf: TabPostPaidOptions[\"priceOf\"],\n logger: Logger,\n): Resolved<PriceQuote> {\n const quoted = call(() => priceOf(request), \"priceOf\", logger);\n if (!quoted.ok) return { kind: \"not-priced\", detail: \"priceOf threw\" };\n const quote = quoted.value;\n if (quote === undefined || quote === null) {\n return { kind: \"not-priced\", detail: \"priceOf returned nothing, so this request is not charged for\" };\n }\n if (!isBytes32(quote.tool)) {\n return { kind: \"price-invalid\", detail: \"quote.tool must be a 32-byte 0x word\" };\n }\n if (!Number.isInteger(quote.units) || quote.units <= 0 || quote.units > 4_294_967_295) {\n return {\n kind: \"price-invalid\",\n detail: `quote.units must be a positive integer inside uint32, received ${String(quote.units)}`,\n };\n }\n if (typeof quote.unitPrice !== \"bigint\" || quote.unitPrice <= 0n) {\n return {\n kind: \"price-invalid\",\n detail: \"quote.unitPrice must be a positive bigint count of Asset base units, never a number\",\n };\n }\n return { kind: \"ok\", value: quote };\n}\n\ntype Called<T> = { ok: true; value: T } | { ok: false; cause: { code: string; message: string } };\n\n/** Runs consumer code without letting it throw into the metering path. */\nfunction call<T>(fn: () => T, what: string, logger: Logger): Called<T> {\n try {\n return { ok: true, value: fn() };\n } catch (thrown) {\n const cause = causeOf(thrown);\n logger.error(`post-paid ${what} threw and was ignored`, cause);\n return { ok: false, cause };\n }\n}\n\ntype CalledAsync<T> = { ok: true; value: T } | { ok: false; thrown: unknown };\n\n/** The asynchronous sibling, keeping a rejected promise out of the metering path. */\nasync function callAsync<T>(\n fn: () => Promise<T>,\n what: string,\n logger: Logger,\n): Promise<CalledAsync<T>> {\n try {\n return { ok: true, value: await fn() };\n } catch (thrown) {\n logger.error(`post-paid ${what} threw and was treated as a refusal of unknown shape`, causeOf(thrown));\n return { ok: false, thrown };\n }\n}\n\n/** Narrows a caught value into the `cause` shape a `TabError` accepts. */\nfunction causeOf(thrown: unknown): { code: string; message: string } {\n if (thrown instanceof Error) return { code: thrown.name, message: thrown.message };\n return { code: \"UNKNOWN\", message: String(thrown) };\n}\n"]}
|