@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,199 @@
1
+ /**
2
+ * Strategy registration and resolution: mechanisms one and two of R23.6.
3
+ *
4
+ * One implementation serves both. A registry is a keyed set of strategies with an
5
+ * optional parent it falls back to:
6
+ *
7
+ * - **Constructor injection.** `createStrategyRegistry({ strategies: [...] })`
8
+ * is what a client wrapper builds from its own options, so a consumer passes
9
+ * strategies in and never touches global state. Its parent is the module-level
10
+ * registry by default, so injection adds to what is already registered instead
11
+ * of hiding it.
12
+ * - **The module-level registry.** `registerPaymentStrategy(strategy)` writes to
13
+ * one process-wide registry, which is what a plugin package needs: it
14
+ * self-registers on import and the consumer's only line is the import.
15
+ *
16
+ * ## Why the module-level state hangs off `globalThis`
17
+ *
18
+ * A plugin package that self-registers on import may resolve a *different copy*
19
+ * of `@tabai/sdk` than the application does, different versions, a nested
20
+ * `node_modules`, a bundled duplicate. With module-scoped state each copy gets
21
+ * its own registry, the plugin registers into a registry the application never
22
+ * reads, and the failure is silent: the strategy simply never resolves. So the
23
+ * state lives under a versioned `Symbol.for` key on `globalThis` and every copy
24
+ * of this module wraps the same `Map`. Only the shared state is global; the
25
+ * behaviour is each copy's own.
26
+ *
27
+ * ## Idempotence, and what replacement means
28
+ *
29
+ * `register` is keyed by `strategy.id`:
30
+ *
31
+ * - the same object again is a no-op, `unchanged`, no warning, no reordering;
32
+ * - a *different* object under a live id replaces it **in its original
33
+ * position** and warns through the logger rather than throwing (design section
34
+ * 9.3), because a strategy replaced at the back of the queue would silently
35
+ * change which strategy resolves for every other Asset too;
36
+ * - anything else appends.
37
+ *
38
+ * Order matters because resolution order is registration order, so the operation
39
+ * has to be idempotent in the ordering as well as in the membership. `Map.set`
40
+ * on a live key keeps the key's insertion position, which is exactly this rule.
41
+ *
42
+ * ## Resolution order
43
+ *
44
+ * An explicit `strategyId` wins. Otherwise the first strategy whose
45
+ * `supports(asset)` returns true, in registration order, own entries before the
46
+ * parent's (design section 9.3). A named strategy that does not support the Asset
47
+ * is an error rather than a silent fall-through to another one: a caller that
48
+ * named a strategy meant it.
49
+ *
50
+ * Requirements: 23.6, 21.5
51
+ */
52
+ import { ok } from "../_shared/index.js";
53
+ import { defaultLogger } from "../logger.js";
54
+ import { notFoundError, validationError } from "../errors.js";
55
+ import { assetKey, supportsAsset, validatePaymentStrategy, } from "./strategy.js";
56
+ /** Versioned so a future state-shape change cannot be misread by an older copy. */
57
+ const MODULE_STATE_KEY = Symbol.for("@tabai/sdk.paymentStrategies.v1");
58
+ function moduleState() {
59
+ const host = globalThis;
60
+ const existing = host[MODULE_STATE_KEY];
61
+ if (existing !== undefined && existing.version === 1)
62
+ return existing;
63
+ const created = { version: 1, strategies: new Map() };
64
+ host[MODULE_STATE_KEY] = created;
65
+ return created;
66
+ }
67
+ /**
68
+ * Builds a registry.
69
+ *
70
+ * Construction cannot fail, so it does not return a `Result`. An invalid entry in
71
+ * `strategies` is refused and reported through the logger, and the registry comes
72
+ * back holding the valid ones, a consumer's typo in one strategy should not take
73
+ * out the strategies that were fine. Use {@link StrategyRegistry.register}
74
+ * directly when the caller wants the `Result` for each one.
75
+ */
76
+ export function createStrategyRegistry(options = {}) {
77
+ const logger = options.logger ?? defaultLogger;
78
+ const own = new Map();
79
+ const registry = registryOver(own, logger, () => resolveParent(options.inherit));
80
+ for (const strategy of options.strategies ?? []) {
81
+ const outcome = registry.register(strategy);
82
+ if (!outcome.ok) {
83
+ logger.error("payment strategy rejected at construction", {
84
+ code: outcome.error.code,
85
+ message: outcome.error.message,
86
+ });
87
+ }
88
+ }
89
+ return registry;
90
+ }
91
+ /** The process-wide registry mechanism two writes to. */
92
+ export function moduleStrategyRegistry(logger) {
93
+ return registryOver(moduleState().strategies, logger ?? defaultLogger, () => undefined);
94
+ }
95
+ /**
96
+ * Registers a strategy process-wide. Idempotent by `strategy.id`.
97
+ *
98
+ * This is the whole of mechanism two: a plugin package calls it at import time,
99
+ * and the consumer's only line is `import "@acme/tab-strategy-solana"`.
100
+ */
101
+ export function registerPaymentStrategy(strategy, options = {}) {
102
+ return moduleStrategyRegistry(options.logger).register(strategy);
103
+ }
104
+ /** Removes a process-wide registration. Returns false when the id was not registered. */
105
+ export const unregisterPaymentStrategy = (id) => moduleStrategyRegistry().unregister(id);
106
+ /** Every process-wide registration, in registration order. */
107
+ export const listPaymentStrategies = () => moduleStrategyRegistry().list();
108
+ /** Empties the process-wide registry. Present for hosts that rebuild their world. */
109
+ export const clearPaymentStrategies = () => moduleStrategyRegistry().clear();
110
+ /** Resolves against the process-wide registry alone. */
111
+ export const resolvePaymentStrategy = (query) => moduleStrategyRegistry().resolve(query);
112
+ function resolveParent(inherit) {
113
+ if (inherit === false)
114
+ return undefined;
115
+ if (inherit !== undefined)
116
+ return inherit;
117
+ return moduleStrategyRegistry();
118
+ }
119
+ /**
120
+ * The one registry implementation, over a `Map` it does not own.
121
+ *
122
+ * `parent` is a thunk so the module-level registry can be the default parent
123
+ * without this function forcing it into existence at construction time, which
124
+ * would make the load order of two modules matter.
125
+ */
126
+ function registryOver(own, logger, parent) {
127
+ const registry = {
128
+ register(candidate) {
129
+ const validated = validatePaymentStrategy(candidate);
130
+ if (!validated.ok)
131
+ return validated;
132
+ const strategy = validated.value;
133
+ const previous = own.get(strategy.id);
134
+ if (previous === strategy) {
135
+ return ok({ action: "unchanged", strategy });
136
+ }
137
+ if (previous !== undefined) {
138
+ // Map.set keeps the key's original insertion position, so the replacement
139
+ // inherits the displaced strategy's place in the resolution order.
140
+ own.set(strategy.id, strategy);
141
+ logger.warn("payment strategy replaced an earlier registration of the same id", {
142
+ strategyId: strategy.id,
143
+ chainIds: strategy.chainIds.map((chainId) => chainId.toString(10)),
144
+ });
145
+ return ok({ action: "replaced", strategy, previous });
146
+ }
147
+ own.set(strategy.id, strategy);
148
+ return ok({ action: "registered", strategy });
149
+ },
150
+ unregister: (id) => own.delete(id),
151
+ clear: () => own.clear(),
152
+ list() {
153
+ const inherited = parent()?.list() ?? [];
154
+ const merged = [...own.values()];
155
+ for (const strategy of inherited) {
156
+ if (!own.has(strategy.id))
157
+ merged.push(strategy);
158
+ }
159
+ return merged;
160
+ },
161
+ get(id) {
162
+ return own.get(id) ?? parent()?.get(id);
163
+ },
164
+ resolve(query) {
165
+ const candidates = registry.list();
166
+ const onThrow = (strategyId) => (error) => logger.warn("payment strategy supports() threw and was read as unsupported", {
167
+ strategyId,
168
+ error: error instanceof Error ? error.message : String(error),
169
+ });
170
+ if (query.strategyId !== undefined) {
171
+ const named = registry.get(query.strategyId);
172
+ if (named === undefined) {
173
+ return notFoundError("STRATEGY_NOT_FOUND", `no payment strategy is registered under id \`${query.strategyId}\``, {
174
+ details: {
175
+ strategyId: query.strategyId,
176
+ registered: candidates.map((strategy) => strategy.id).join(", "),
177
+ },
178
+ });
179
+ }
180
+ if (!supportsAsset(named, query.asset, onThrow(named.id))) {
181
+ return validationError("STRATEGY_ASSET_MISMATCH", `payment strategy \`${named.id}\` does not support asset ${assetKey(query.asset)}`, { details: { strategyId: named.id, asset: assetKey(query.asset) } });
182
+ }
183
+ return ok(named);
184
+ }
185
+ for (const strategy of candidates) {
186
+ if (supportsAsset(strategy, query.asset, onThrow(strategy.id)))
187
+ return ok(strategy);
188
+ }
189
+ return notFoundError("NO_STRATEGY_FOR_ASSET", `no registered payment strategy supports asset ${assetKey(query.asset)}`, {
190
+ details: {
191
+ asset: assetKey(query.asset),
192
+ registered: candidates.map((strategy) => strategy.id).join(", "),
193
+ },
194
+ });
195
+ },
196
+ };
197
+ return registry;
198
+ }
199
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/payments/registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Strategy registration and resolution: mechanisms one and two of R23.6.\n *\n * One implementation serves both. A registry is a keyed set of strategies with an\n * optional parent it falls back to:\n *\n * - **Constructor injection.** `createStrategyRegistry({ strategies: [...] })`\n * is what a client wrapper builds from its own options, so a consumer passes\n * strategies in and never touches global state. Its parent is the module-level\n * registry by default, so injection adds to what is already registered instead\n * of hiding it.\n * - **The module-level registry.** `registerPaymentStrategy(strategy)` writes to\n * one process-wide registry, which is what a plugin package needs: it\n * self-registers on import and the consumer's only line is the import.\n *\n * ## Why the module-level state hangs off `globalThis`\n *\n * A plugin package that self-registers on import may resolve a *different copy*\n * of `@tabai/sdk` than the application does, different versions, a nested\n * `node_modules`, a bundled duplicate. With module-scoped state each copy gets\n * its own registry, the plugin registers into a registry the application never\n * reads, and the failure is silent: the strategy simply never resolves. So the\n * state lives under a versioned `Symbol.for` key on `globalThis` and every copy\n * of this module wraps the same `Map`. Only the shared state is global; the\n * behaviour is each copy's own.\n *\n * ## Idempotence, and what replacement means\n *\n * `register` is keyed by `strategy.id`:\n *\n * - the same object again is a no-op, `unchanged`, no warning, no reordering;\n * - a *different* object under a live id replaces it **in its original\n * position** and warns through the logger rather than throwing (design section\n * 9.3), because a strategy replaced at the back of the queue would silently\n * change which strategy resolves for every other Asset too;\n * - anything else appends.\n *\n * Order matters because resolution order is registration order, so the operation\n * has to be idempotent in the ordering as well as in the membership. `Map.set`\n * on a live key keeps the key's insertion position, which is exactly this rule.\n *\n * ## Resolution order\n *\n * An explicit `strategyId` wins. Otherwise the first strategy whose\n * `supports(asset)` returns true, in registration order, own entries before the\n * parent's (design section 9.3). A named strategy that does not support the Asset\n * is an error rather than a silent fall-through to another one: a caller that\n * named a strategy meant it.\n *\n * Requirements: 23.6, 21.5\n */\n\nimport { ok, type Result } from \"../_shared/index.js\";\nimport { defaultLogger, type Logger } from \"../logger.js\";\nimport { notFoundError, validationError } from \"../errors.js\";\nimport {\n assetKey,\n supportsAsset,\n validatePaymentStrategy,\n type AssetRef,\n type PaymentStrategy,\n} from \"./strategy.js\";\n\n/** What `register` did. `unchanged` is the idempotent case. */\nexport type RegistrationAction = \"registered\" | \"replaced\" | \"unchanged\";\n\nexport interface StrategyRegistration {\n readonly action: RegistrationAction;\n readonly strategy: PaymentStrategy;\n /** The strategy that was displaced, on `replaced` alone. */\n readonly previous?: PaymentStrategy;\n}\n\n/** What resolution is given: the Asset that must be settled, and an optional override. */\nexport interface StrategyQuery {\n readonly asset: AssetRef;\n readonly strategyId?: string;\n}\n\nexport interface StrategyRegistry {\n register(strategy: PaymentStrategy): Result<StrategyRegistration>;\n /** Removes a strategy from *this* registry. Never reaches into the parent. */\n unregister(id: string): boolean;\n /** Empties *this* registry. Never reaches into the parent. */\n clear(): void;\n /** This registry's own strategies in registration order, then the parent's. */\n list(): readonly PaymentStrategy[];\n get(id: string): PaymentStrategy | undefined;\n resolve(query: StrategyQuery): Result<PaymentStrategy>;\n}\n\nexport interface StrategyRegistryOptions {\n /** Registered in array order at construction. Mechanism one. */\n readonly strategies?: readonly PaymentStrategy[];\n readonly logger?: Logger;\n /**\n * The registry to fall back to. Omitted means the module-level registry;\n * `false` means an isolated registry, which is what the module-level registry\n * itself is and what a test wants.\n */\n readonly inherit?: StrategyRegistry | false;\n}\n\n/** Versioned so a future state-shape change cannot be misread by an older copy. */\nconst MODULE_STATE_KEY = Symbol.for(\"@tabai/sdk.paymentStrategies.v1\");\n\ninterface ModuleState {\n readonly version: 1;\n readonly strategies: Map<string, PaymentStrategy>;\n}\n\nfunction moduleState(): ModuleState {\n const host = globalThis as typeof globalThis & { [MODULE_STATE_KEY]?: ModuleState };\n const existing = host[MODULE_STATE_KEY];\n if (existing !== undefined && existing.version === 1) return existing;\n const created: ModuleState = { version: 1, strategies: new Map() };\n host[MODULE_STATE_KEY] = created;\n return created;\n}\n\n/**\n * Builds a registry.\n *\n * Construction cannot fail, so it does not return a `Result`. An invalid entry in\n * `strategies` is refused and reported through the logger, and the registry comes\n * back holding the valid ones, a consumer's typo in one strategy should not take\n * out the strategies that were fine. Use {@link StrategyRegistry.register}\n * directly when the caller wants the `Result` for each one.\n */\nexport function createStrategyRegistry(options: StrategyRegistryOptions = {}): StrategyRegistry {\n const logger = options.logger ?? defaultLogger;\n const own = new Map<string, PaymentStrategy>();\n const registry = registryOver(own, logger, () => resolveParent(options.inherit));\n\n for (const strategy of options.strategies ?? []) {\n const outcome = registry.register(strategy);\n if (!outcome.ok) {\n logger.error(\"payment strategy rejected at construction\", {\n code: outcome.error.code,\n message: outcome.error.message,\n });\n }\n }\n return registry;\n}\n\n/** The process-wide registry mechanism two writes to. */\nexport function moduleStrategyRegistry(logger?: Logger): StrategyRegistry {\n return registryOver(moduleState().strategies, logger ?? defaultLogger, () => undefined);\n}\n\n/**\n * Registers a strategy process-wide. Idempotent by `strategy.id`.\n *\n * This is the whole of mechanism two: a plugin package calls it at import time,\n * and the consumer's only line is `import \"@acme/tab-strategy-solana\"`.\n */\nexport function registerPaymentStrategy(\n strategy: PaymentStrategy,\n options: { readonly logger?: Logger } = {},\n): Result<StrategyRegistration> {\n return moduleStrategyRegistry(options.logger).register(strategy);\n}\n\n/** Removes a process-wide registration. Returns false when the id was not registered. */\nexport const unregisterPaymentStrategy = (id: string): boolean =>\n moduleStrategyRegistry().unregister(id);\n\n/** Every process-wide registration, in registration order. */\nexport const listPaymentStrategies = (): readonly PaymentStrategy[] =>\n moduleStrategyRegistry().list();\n\n/** Empties the process-wide registry. Present for hosts that rebuild their world. */\nexport const clearPaymentStrategies = (): void => moduleStrategyRegistry().clear();\n\n/** Resolves against the process-wide registry alone. */\nexport const resolvePaymentStrategy = (query: StrategyQuery): Result<PaymentStrategy> =>\n moduleStrategyRegistry().resolve(query);\n\nfunction resolveParent(inherit: StrategyRegistry | false | undefined): StrategyRegistry | undefined {\n if (inherit === false) return undefined;\n if (inherit !== undefined) return inherit;\n return moduleStrategyRegistry();\n}\n\n/**\n * The one registry implementation, over a `Map` it does not own.\n *\n * `parent` is a thunk so the module-level registry can be the default parent\n * without this function forcing it into existence at construction time, which\n * would make the load order of two modules matter.\n */\nfunction registryOver(\n own: Map<string, PaymentStrategy>,\n logger: Logger,\n parent: () => StrategyRegistry | undefined,\n): StrategyRegistry {\n const registry: StrategyRegistry = {\n register(candidate) {\n const validated = validatePaymentStrategy(candidate);\n if (!validated.ok) return validated;\n const strategy = validated.value;\n const previous = own.get(strategy.id);\n\n if (previous === strategy) {\n return ok({ action: \"unchanged\", strategy });\n }\n if (previous !== undefined) {\n // Map.set keeps the key's original insertion position, so the replacement\n // inherits the displaced strategy's place in the resolution order.\n own.set(strategy.id, strategy);\n logger.warn(\"payment strategy replaced an earlier registration of the same id\", {\n strategyId: strategy.id,\n chainIds: strategy.chainIds.map((chainId) => chainId.toString(10)),\n });\n return ok({ action: \"replaced\", strategy, previous });\n }\n own.set(strategy.id, strategy);\n return ok({ action: \"registered\", strategy });\n },\n\n unregister: (id) => own.delete(id),\n\n clear: () => own.clear(),\n\n list() {\n const inherited = parent()?.list() ?? [];\n const merged = [...own.values()];\n for (const strategy of inherited) {\n if (!own.has(strategy.id)) merged.push(strategy);\n }\n return merged;\n },\n\n get(id) {\n return own.get(id) ?? parent()?.get(id);\n },\n\n resolve(query) {\n const candidates = registry.list();\n const onThrow = (strategyId: string) => (error: unknown) =>\n logger.warn(\"payment strategy supports() threw and was read as unsupported\", {\n strategyId,\n error: error instanceof Error ? error.message : String(error),\n });\n\n if (query.strategyId !== undefined) {\n const named = registry.get(query.strategyId);\n if (named === undefined) {\n return notFoundError(\n \"STRATEGY_NOT_FOUND\",\n `no payment strategy is registered under id \\`${query.strategyId}\\``,\n {\n details: {\n strategyId: query.strategyId,\n registered: candidates.map((strategy) => strategy.id).join(\", \"),\n },\n },\n );\n }\n if (!supportsAsset(named, query.asset, onThrow(named.id))) {\n return validationError(\n \"STRATEGY_ASSET_MISMATCH\",\n `payment strategy \\`${named.id}\\` does not support asset ${assetKey(query.asset)}`,\n { details: { strategyId: named.id, asset: assetKey(query.asset) } },\n );\n }\n return ok(named);\n }\n\n for (const strategy of candidates) {\n if (supportsAsset(strategy, query.asset, onThrow(strategy.id))) return ok(strategy);\n }\n return notFoundError(\n \"NO_STRATEGY_FOR_ASSET\",\n `no registered payment strategy supports asset ${assetKey(query.asset)}`,\n {\n details: {\n asset: assetKey(query.asset),\n registered: candidates.map((strategy) => strategy.id).join(\", \"),\n },\n },\n );\n },\n };\n\n return registry;\n}\n"]}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The payment-strategy seam.
3
+ *
4
+ * A strategy is how an Agent pays down an Open Tab. On Monad there is exactly one
5
+ * way: approve the Asset and call `TabSettlement.settle`, which moves the Asset to
6
+ * the Service and applies the Settlement in the same transaction. The seam still
7
+ * exists so that a different signer, a smart account, a session key, or a test
8
+ * double can stand behind the same call without the SDK caring which.
9
+ *
10
+ * ## What a strategy does not do
11
+ *
12
+ * It does not meter, it does not read tabs, and it does not decide amounts. It is
13
+ * handed a fully specified charge and returns either a receipt or a typed error.
14
+ * Every figure crossing this seam is a `bigint` count of Asset base units, never a
15
+ * number and never a decimal string.
16
+ */
17
+ import { type Address, type Bytes32, type Hex, type Result } from "../_shared/index.js";
18
+ /** One Asset a tab can be denominated in. `chainId` is the EVM chain id. */
19
+ export interface AssetRef {
20
+ readonly chainId: bigint;
21
+ readonly address: Address;
22
+ readonly decimals: number;
23
+ readonly symbol: string;
24
+ }
25
+ /** What a Service is asking to be paid, before any strategy is chosen. */
26
+ export interface ChargeRequest {
27
+ readonly agent: Address;
28
+ readonly serviceId: Bytes32;
29
+ readonly asset: AssetRef;
30
+ readonly amount: bigint;
31
+ }
32
+ /** A charge the strategy has been asked to pay. The surface resolves where the money goes. */
33
+ export type SettleRequest = ChargeRequest;
34
+ export interface ChargeQuote {
35
+ readonly amount: bigint;
36
+ readonly asset: AssetRef;
37
+ readonly feeNote: string;
38
+ }
39
+ /**
40
+ * What came back from a Settlement.
41
+ *
42
+ * `settlementId`, `applied` and `toPrepaid` are read from the `Settled` event in the
43
+ * transaction receipt. They are `null` only when the signer could not wait for the
44
+ * receipt, in which case the transaction hash still names the payment and the registry
45
+ * will carry the figures once it indexes the block.
46
+ */
47
+ export interface SettlementReceipt {
48
+ readonly strategyId: string;
49
+ readonly chainId: bigint;
50
+ readonly txHash: Hex;
51
+ readonly asset: AssetRef;
52
+ readonly amount: bigint;
53
+ readonly payer: Address;
54
+ readonly serviceId: Bytes32;
55
+ readonly settlementId: Bytes32 | null;
56
+ readonly applied: bigint | null;
57
+ readonly toPrepaid: bigint | null;
58
+ readonly submittedAt: number;
59
+ readonly batchIndex?: number;
60
+ }
61
+ export interface PaymentStrategy {
62
+ readonly id: string;
63
+ /** The chain ids this strategy can settle on. */
64
+ readonly chainIds: readonly bigint[];
65
+ supports(asset: AssetRef): boolean;
66
+ quote(request: ChargeRequest): Promise<Result<ChargeQuote>>;
67
+ settle(request: SettleRequest): Promise<Result<SettlementReceipt>>;
68
+ settleBatch?(requests: readonly SettleRequest[]): Promise<Result<readonly SettlementReceipt[]>>;
69
+ }
70
+ /** The registry key for an Asset: `<chainId>:<address>`, lower-cased. */
71
+ export declare const assetKey: (asset: AssetRef) => string;
72
+ export declare const sameAsset: (a: AssetRef, b: AssetRef) => boolean;
73
+ /** A 20-byte address as a 32-byte topic. */
74
+ export declare const addressTopic: (address: Address) => Bytes32;
75
+ export declare function supportsAsset(strategy: PaymentStrategy, asset: AssetRef, onThrow?: (error: unknown) => void): boolean;
76
+ export declare function validateAssetRef(value: unknown, label: string): Result<AssetRef>;
77
+ export declare function validatePaymentStrategy(value: unknown, label?: string): Result<PaymentStrategy>;
78
+ export declare function validateChargeRequest(request: ChargeRequest): Result<ChargeRequest>;
79
+ export declare const validateSettleRequest: typeof validateChargeRequest;
80
+ //# sourceMappingURL=strategy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../src/payments/strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAA4B,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAG5G,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,8FAA8F;AAC9F,MAAM,MAAM,aAAa,GAAG,aAAa,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;IACnC,KAAK,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5D,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACnE,WAAW,CAAC,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC,CAAC;CACjG;AAED,yEAAyE;AACzE,eAAO,MAAM,QAAQ,GAAI,OAAO,QAAQ,KAAG,MACqB,CAAC;AAEjE,eAAO,MAAM,SAAS,GAAI,GAAG,QAAQ,EAAE,GAAG,QAAQ,KAAG,OAAsC,CAAC;AAE5F,4CAA4C;AAC5C,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,OACS,CAAC;AAE1D,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,QAAQ,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,OAAO,CAOT;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,CAsBhF;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,SAAa,GAAG,MAAM,CAAC,eAAe,CAAC,CAgCnG;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC,CAsBnF;AAED,eAAO,MAAM,qBAAqB,8BAAwB,CAAC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The payment-strategy seam.
3
+ *
4
+ * A strategy is how an Agent pays down an Open Tab. On Monad there is exactly one
5
+ * way: approve the Asset and call `TabSettlement.settle`, which moves the Asset to
6
+ * the Service and applies the Settlement in the same transaction. The seam still
7
+ * exists so that a different signer, a smart account, a session key, or a test
8
+ * double can stand behind the same call without the SDK caring which.
9
+ *
10
+ * ## What a strategy does not do
11
+ *
12
+ * It does not meter, it does not read tabs, and it does not decide amounts. It is
13
+ * handed a fully specified charge and returns either a receipt or a typed error.
14
+ * Every figure crossing this seam is a `bigint` count of Asset base units, never a
15
+ * number and never a decimal string.
16
+ */
17
+ import { isAddress, isBytes32, ok } from "../_shared/index.js";
18
+ import { validationError } from "../errors.js";
19
+ /** The registry key for an Asset: `<chainId>:<address>`, lower-cased. */
20
+ export const assetKey = (asset) => `${asset.chainId.toString(10)}:${asset.address.toLowerCase()}`;
21
+ export const sameAsset = (a, b) => assetKey(a) === assetKey(b);
22
+ /** A 20-byte address as a 32-byte topic. */
23
+ export const addressTopic = (address) => `0x${address.slice(2).toLowerCase().padStart(64, "0")}`;
24
+ export function supportsAsset(strategy, asset, onThrow) {
25
+ try {
26
+ return strategy.supports(asset) === true;
27
+ }
28
+ catch (error) {
29
+ onThrow?.(error);
30
+ return false;
31
+ }
32
+ }
33
+ export function validateAssetRef(value, label) {
34
+ if (typeof value !== "object" || value === null) {
35
+ return validationError("ASSET_REF_INVALID", `${label} must be an object describing one Asset`);
36
+ }
37
+ const candidate = value;
38
+ if (typeof candidate.chainId !== "bigint" || candidate.chainId <= 0n) {
39
+ return validationError("ASSET_REF_INVALID", `${label}.chainId must be a positive bigint EVM chain id`);
40
+ }
41
+ if (!isAddress(candidate.address)) {
42
+ return validationError("ASSET_REF_INVALID", `${label}.address must be a 20-byte 0x address`);
43
+ }
44
+ if (typeof candidate.decimals !== "number" ||
45
+ !Number.isInteger(candidate.decimals) ||
46
+ candidate.decimals < 0) {
47
+ return validationError("ASSET_REF_INVALID", `${label}.decimals must be a non-negative integer`);
48
+ }
49
+ if (typeof candidate.symbol !== "string" || candidate.symbol.length === 0) {
50
+ return validationError("ASSET_REF_INVALID", `${label}.symbol must be a non-empty string`);
51
+ }
52
+ return ok(candidate);
53
+ }
54
+ export function validatePaymentStrategy(value, label = "strategy") {
55
+ if (typeof value !== "object" || value === null) {
56
+ return validationError("STRATEGY_INVALID", `${label} must be an object implementing PaymentStrategy`);
57
+ }
58
+ const candidate = value;
59
+ if (typeof candidate.id !== "string" || candidate.id.trim().length === 0) {
60
+ return validationError("STRATEGY_INVALID", `${label}.id must be a non-empty string`);
61
+ }
62
+ const id = candidate.id;
63
+ if (!Array.isArray(candidate.chainIds) || candidate.chainIds.length === 0) {
64
+ return validationError("STRATEGY_INVALID", `${label} \`${id}\` must declare at least one chain id in chainIds`);
65
+ }
66
+ for (const chainId of candidate.chainIds) {
67
+ if (typeof chainId !== "bigint") {
68
+ return validationError("STRATEGY_INVALID", `${label} \`${id}\` must declare every chain id as a bigint, received ${typeof chainId}`);
69
+ }
70
+ }
71
+ for (const method of ["supports", "quote", "settle"]) {
72
+ if (typeof candidate[method] !== "function") {
73
+ return validationError("STRATEGY_INVALID", `${label} \`${id}\` must implement ${method}()`);
74
+ }
75
+ }
76
+ if (candidate.settleBatch !== undefined && typeof candidate.settleBatch !== "function") {
77
+ return validationError("STRATEGY_INVALID", `${label} \`${id}\` declares settleBatch but it is not a function; leave it undefined when there is no batch form`);
78
+ }
79
+ return ok(candidate);
80
+ }
81
+ export function validateChargeRequest(request) {
82
+ if (!isAddress(request.agent)) {
83
+ return validationError("CHARGE_INVALID", "request.agent must be a 20-byte 0x address");
84
+ }
85
+ if (!isBytes32(request.serviceId)) {
86
+ return validationError("CHARGE_INVALID", "request.serviceId must be a 32-byte 0x word");
87
+ }
88
+ const asset = validateAssetRef(request.asset, "request.asset");
89
+ if (!asset.ok)
90
+ return asset;
91
+ if (typeof request.amount !== "bigint") {
92
+ return validationError("CHARGE_INVALID", "request.amount must be a bigint count of Asset base units, never a number");
93
+ }
94
+ if (request.amount <= 0n) {
95
+ return validationError("AMOUNT_NOT_POSITIVE", "request.amount must be greater than zero base units");
96
+ }
97
+ if (request.amount > (1n << 128n) - 1n) {
98
+ return validationError("AMOUNT_OUT_OF_RANGE", "request.amount must fit a uint128");
99
+ }
100
+ return ok(request);
101
+ }
102
+ export const validateSettleRequest = validateChargeRequest;
103
+ //# sourceMappingURL=strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strategy.js","sourceRoot":"","sources":["../../src/payments/strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAqD,MAAM,eAAe,CAAC;AAC5G,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AA4D/C,yEAAyE;AACzE,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAe,EAAU,EAAE,CAClD,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;AAEjE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAW,EAAE,CAAW,EAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;AAE5F,4CAA4C;AAC5C,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAgB,EAAW,EAAE,CACxD,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC;AAE1D,MAAM,UAAU,aAAa,CAC3B,QAAyB,EACzB,KAAe,EACf,OAAkC;IAElC,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc,EAAE,KAAa;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,eAAe,CAAC,mBAAmB,EAAE,GAAG,KAAK,yCAAyC,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,SAAS,GAAG,KAA0B,CAAC;IAC7C,IAAI,OAAO,SAAS,CAAC,OAAO,KAAK,QAAQ,IAAI,SAAS,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;QACrE,OAAO,eAAe,CAAC,mBAAmB,EAAE,GAAG,KAAK,iDAAiD,CAAC,CAAC;IACzG,CAAC;IACD,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAClC,OAAO,eAAe,CAAC,mBAAmB,EAAE,GAAG,KAAK,uCAAuC,CAAC,CAAC;IAC/F,CAAC;IACD,IACE,OAAO,SAAS,CAAC,QAAQ,KAAK,QAAQ;QACtC,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC;QACrC,SAAS,CAAC,QAAQ,GAAG,CAAC,EACtB,CAAC;QACD,OAAO,eAAe,CAAC,mBAAmB,EAAE,GAAG,KAAK,0CAA0C,CAAC,CAAC;IAClG,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,MAAM,KAAK,QAAQ,IAAI,SAAS,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1E,OAAO,eAAe,CAAC,mBAAmB,EAAE,GAAG,KAAK,oCAAoC,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,EAAE,CAAC,SAAqB,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAc,EAAE,KAAK,GAAG,UAAU;IACxE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,OAAO,eAAe,CAAC,kBAAkB,EAAE,GAAG,KAAK,iDAAiD,CAAC,CAAC;IACxG,CAAC;IACD,MAAM,SAAS,GAAG,KAAiC,CAAC;IACpD,IAAI,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ,IAAI,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzE,OAAO,eAAe,CAAC,kBAAkB,EAAE,GAAG,KAAK,gCAAgC,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,EAAE,GAAG,SAAS,CAAC,EAAE,CAAC;IACxB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1E,OAAO,eAAe,CAAC,kBAAkB,EAAE,GAAG,KAAK,MAAM,EAAE,mDAAmD,CAAC,CAAC;IAClH,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,SAAS,CAAC,QAAQ,EAAE,CAAC;QACzC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,eAAe,CACpB,kBAAkB,EAClB,GAAG,KAAK,MAAM,EAAE,wDAAwD,OAAO,OAAO,EAAE,CACzF,CAAC;QACJ,CAAC;IACH,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAU,EAAE,CAAC;QAC9D,IAAI,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC;YAC5C,OAAO,eAAe,CAAC,kBAAkB,EAAE,GAAG,KAAK,MAAM,EAAE,qBAAqB,MAAM,IAAI,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IACD,IAAI,SAAS,CAAC,WAAW,KAAK,SAAS,IAAI,OAAO,SAAS,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;QACvF,OAAO,eAAe,CACpB,kBAAkB,EAClB,GAAG,KAAK,MAAM,EAAE,kGAAkG,CACnH,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC,SAA4B,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,eAAe,CAAC,gBAAgB,EAAE,4CAA4C,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAClC,OAAO,eAAe,CAAC,gBAAgB,EAAE,6CAA6C,CAAC,CAAC;IAC1F,CAAC;IACD,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,eAAe,CACpB,gBAAgB,EAChB,2EAA2E,CAC5E,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QACzB,OAAO,eAAe,CAAC,qBAAqB,EAAE,qDAAqD,CAAC,CAAC;IACvG,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACvC,OAAO,eAAe,CAAC,qBAAqB,EAAE,mCAAmC,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC","sourcesContent":["/**\n * The payment-strategy seam.\n *\n * A strategy is how an Agent pays down an Open Tab. On Monad there is exactly one\n * way: approve the Asset and call `TabSettlement.settle`, which moves the Asset to\n * the Service and applies the Settlement in the same transaction. The seam still\n * exists so that a different signer, a smart account, a session key, or a test\n * double can stand behind the same call without the SDK caring which.\n *\n * ## What a strategy does not do\n *\n * It does not meter, it does not read tabs, and it does not decide amounts. It is\n * handed a fully specified charge and returns either a receipt or a typed error.\n * Every figure crossing this seam is a `bigint` count of Asset base units, never a\n * number and never a decimal string.\n */\nimport { isAddress, isBytes32, ok, type Address, type Bytes32, type Hex, type Result } from \"../_shared/index.js\";\nimport { validationError } from \"../errors.js\";\n\n/** One Asset a tab can be denominated in. `chainId` is the EVM chain id. */\nexport interface AssetRef {\n readonly chainId: bigint;\n readonly address: Address;\n readonly decimals: number;\n readonly symbol: string;\n}\n\n/** What a Service is asking to be paid, before any strategy is chosen. */\nexport interface ChargeRequest {\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: AssetRef;\n readonly amount: bigint;\n}\n\n/** A charge the strategy has been asked to pay. The surface resolves where the money goes. */\nexport type SettleRequest = ChargeRequest;\n\nexport interface ChargeQuote {\n readonly amount: bigint;\n readonly asset: AssetRef;\n readonly feeNote: string;\n}\n\n/**\n * What came back from a Settlement.\n *\n * `settlementId`, `applied` and `toPrepaid` are read from the `Settled` event in the\n * transaction receipt. They are `null` only when the signer could not wait for the\n * receipt, in which case the transaction hash still names the payment and the registry\n * will carry the figures once it indexes the block.\n */\nexport interface SettlementReceipt {\n readonly strategyId: string;\n readonly chainId: bigint;\n readonly txHash: Hex;\n readonly asset: AssetRef;\n readonly amount: bigint;\n readonly payer: Address;\n readonly serviceId: Bytes32;\n readonly settlementId: Bytes32 | null;\n readonly applied: bigint | null;\n readonly toPrepaid: bigint | null;\n readonly submittedAt: number;\n readonly batchIndex?: number;\n}\n\nexport interface PaymentStrategy {\n readonly id: string;\n /** The chain ids this strategy can settle on. */\n readonly chainIds: readonly bigint[];\n supports(asset: AssetRef): boolean;\n quote(request: ChargeRequest): Promise<Result<ChargeQuote>>;\n settle(request: SettleRequest): Promise<Result<SettlementReceipt>>;\n settleBatch?(requests: readonly SettleRequest[]): Promise<Result<readonly SettlementReceipt[]>>;\n}\n\n/** The registry key for an Asset: `<chainId>:<address>`, lower-cased. */\nexport const assetKey = (asset: AssetRef): string =>\n `${asset.chainId.toString(10)}:${asset.address.toLowerCase()}`;\n\nexport const sameAsset = (a: AssetRef, b: AssetRef): boolean => assetKey(a) === assetKey(b);\n\n/** A 20-byte address as a 32-byte topic. */\nexport const addressTopic = (address: Address): Bytes32 =>\n `0x${address.slice(2).toLowerCase().padStart(64, \"0\")}`;\n\nexport function supportsAsset(\n strategy: PaymentStrategy,\n asset: AssetRef,\n onThrow?: (error: unknown) => void,\n): boolean {\n try {\n return strategy.supports(asset) === true;\n } catch (error) {\n onThrow?.(error);\n return false;\n }\n}\n\nexport function validateAssetRef(value: unknown, label: string): Result<AssetRef> {\n if (typeof value !== \"object\" || value === null) {\n return validationError(\"ASSET_REF_INVALID\", `${label} must be an object describing one Asset`);\n }\n const candidate = value as Partial<AssetRef>;\n if (typeof candidate.chainId !== \"bigint\" || candidate.chainId <= 0n) {\n return validationError(\"ASSET_REF_INVALID\", `${label}.chainId must be a positive bigint EVM chain id`);\n }\n if (!isAddress(candidate.address)) {\n return validationError(\"ASSET_REF_INVALID\", `${label}.address must be a 20-byte 0x address`);\n }\n if (\n typeof candidate.decimals !== \"number\" ||\n !Number.isInteger(candidate.decimals) ||\n candidate.decimals < 0\n ) {\n return validationError(\"ASSET_REF_INVALID\", `${label}.decimals must be a non-negative integer`);\n }\n if (typeof candidate.symbol !== \"string\" || candidate.symbol.length === 0) {\n return validationError(\"ASSET_REF_INVALID\", `${label}.symbol must be a non-empty string`);\n }\n return ok(candidate as AssetRef);\n}\n\nexport function validatePaymentStrategy(value: unknown, label = \"strategy\"): Result<PaymentStrategy> {\n if (typeof value !== \"object\" || value === null) {\n return validationError(\"STRATEGY_INVALID\", `${label} must be an object implementing PaymentStrategy`);\n }\n const candidate = value as Partial<PaymentStrategy>;\n if (typeof candidate.id !== \"string\" || candidate.id.trim().length === 0) {\n return validationError(\"STRATEGY_INVALID\", `${label}.id must be a non-empty string`);\n }\n const id = candidate.id;\n if (!Array.isArray(candidate.chainIds) || candidate.chainIds.length === 0) {\n return validationError(\"STRATEGY_INVALID\", `${label} \\`${id}\\` must declare at least one chain id in chainIds`);\n }\n for (const chainId of candidate.chainIds) {\n if (typeof chainId !== \"bigint\") {\n return validationError(\n \"STRATEGY_INVALID\",\n `${label} \\`${id}\\` must declare every chain id as a bigint, received ${typeof chainId}`,\n );\n }\n }\n for (const method of [\"supports\", \"quote\", \"settle\"] as const) {\n if (typeof candidate[method] !== \"function\") {\n return validationError(\"STRATEGY_INVALID\", `${label} \\`${id}\\` must implement ${method}()`);\n }\n }\n if (candidate.settleBatch !== undefined && typeof candidate.settleBatch !== \"function\") {\n return validationError(\n \"STRATEGY_INVALID\",\n `${label} \\`${id}\\` declares settleBatch but it is not a function; leave it undefined when there is no batch form`,\n );\n }\n return ok(candidate as PaymentStrategy);\n}\n\nexport function validateChargeRequest(request: ChargeRequest): Result<ChargeRequest> {\n if (!isAddress(request.agent)) {\n return validationError(\"CHARGE_INVALID\", \"request.agent must be a 20-byte 0x address\");\n }\n if (!isBytes32(request.serviceId)) {\n return validationError(\"CHARGE_INVALID\", \"request.serviceId must be a 32-byte 0x word\");\n }\n const asset = validateAssetRef(request.asset, \"request.asset\");\n if (!asset.ok) return asset;\n if (typeof request.amount !== \"bigint\") {\n return validationError(\n \"CHARGE_INVALID\",\n \"request.amount must be a bigint count of Asset base units, never a number\",\n );\n }\n if (request.amount <= 0n) {\n return validationError(\"AMOUNT_NOT_POSITIVE\", \"request.amount must be greater than zero base units\");\n }\n if (request.amount > (1n << 128n) - 1n) {\n return validationError(\"AMOUNT_OUT_OF_RANGE\", \"request.amount must fit a uint128\");\n }\n return ok(request);\n}\n\nexport const validateSettleRequest = validateChargeRequest;\n"]}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The hook seam of the proxy layer (R23.4).
3
+ *
4
+ * A hook is a named pair of optional phases. `before` runs ahead of the proxied
5
+ * request, in registration order; `after` runs once the response exists, in
6
+ * reverse order, so the hook registered first is the last to see the response
7
+ * on the way out. That is the same nesting a middleware stack has, expressed as
8
+ * two lists rather than as recursion, because two lists are what a Service
9
+ * reads back when it asks "which hooks ran, and in what order".
10
+ *
11
+ * ## A hook cannot fail a request unless it says so
12
+ *
13
+ * Every phase returns a `Result`. An `err` is logged with the hook's name and
14
+ * the phase, and the request carries on. That is the default because a hook is
15
+ * observation: it logs, it annotates, it looks something up. A Service whose
16
+ * log shipper is down should still deliver work. The one exception is a hook
17
+ * that declares `critical: true`, whose `err` replaces the response with the
18
+ * error it returned, at the HTTP status its category maps to. A hook that
19
+ * *throws* is treated exactly as one that returned an `INTERNAL` error, so the
20
+ * distinction between "misbehaved" and "reported a problem" is the `critical`
21
+ * flag and never the control flow.
22
+ *
23
+ * ## What a hook can see and what it can change
24
+ *
25
+ * The context is one object per request, shared by both phases and every hook,
26
+ * with two writable parts. `settlement` is where a hook that looked a Settlement
27
+ * up on Monad attaches what it found (R23.5), and `state` is scratch a hook uses
28
+ * to hand something from its `before` to its `after` phase. A hook
29
+ * cannot replace the request or the response through the context; only a
30
+ * critical failure changes what goes out.
31
+ *
32
+ * Requirements: 23.4, 23.5
33
+ */
34
+ import type { Result } from "../_shared/index.js";
35
+ import type { Logger } from "../logger.js";
36
+ import type { MeteredCharge, MeteringOutcome } from "../server/post-paid.js";
37
+ import type { Address, Bytes32, Hex } from "../_shared/index.js";
38
+ /**
39
+ * A Settlement as a hook may attach it to a request: the figures `TabBook`
40
+ * recorded and the Monad transaction that carried them.
41
+ */
42
+ export interface SettlementView {
43
+ readonly settlementId: Bytes32;
44
+ readonly txHash: Hex;
45
+ readonly agent: Address;
46
+ readonly serviceId: Bytes32;
47
+ readonly asset: Address;
48
+ readonly amount: bigint;
49
+ readonly applied: bigint;
50
+ readonly toPrepaid: bigint;
51
+ readonly blockNumber?: number;
52
+ }
53
+ export type ProxyPhase = "before" | "after";
54
+ export interface ProxyHookContext {
55
+ readonly phase: ProxyPhase;
56
+ /** The request as it reached the proxy, before any header was dropped for forwarding. */
57
+ readonly request: Request;
58
+ /** The response about to be returned. Present in the `after` phase only. */
59
+ readonly response?: Response;
60
+ /**
61
+ * The delivery the metering plugin recorded for this request, when it recorded
62
+ * one. Absent when the request was not metered, when metering refused it, and
63
+ * under `release: "before-metering"`, where the recording is still pending
64
+ * while the hooks run.
65
+ */
66
+ readonly charge?: MeteredCharge;
67
+ /** The metering outcome in full, present whenever the plugin reached one before the hooks ran. */
68
+ readonly metering?: MeteringOutcome;
69
+ /**
70
+ * The Settlement attached to this request, if a hook found one.
71
+ * Writable: attaching it is a lookup hook's job (R23.5).
72
+ */
73
+ settlement?: SettlementView;
74
+ /** Scratch for the life of one request. Convention: keyed by hook name. */
75
+ readonly state: Map<string, unknown>;
76
+ readonly logger: Logger;
77
+ }
78
+ export interface ProxyHook {
79
+ /** Names the hook in logs and in `state`. Unique within one proxy. */
80
+ readonly name: string;
81
+ /**
82
+ * When true, an `err` from either phase fails the request with that error.
83
+ * Defaults to false: a hook observes, and observation failing is not a reason
84
+ * to withhold delivered work.
85
+ */
86
+ readonly critical?: boolean;
87
+ before?(context: ProxyHookContext): Promise<Result<void>>;
88
+ after?(context: ProxyHookContext): Promise<Result<void>>;
89
+ }
90
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/proxy/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,kGAAkG;IAClG,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1D,KAAK,CAAC,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;CAC1D"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The hook seam of the proxy layer (R23.4).
3
+ *
4
+ * A hook is a named pair of optional phases. `before` runs ahead of the proxied
5
+ * request, in registration order; `after` runs once the response exists, in
6
+ * reverse order, so the hook registered first is the last to see the response
7
+ * on the way out. That is the same nesting a middleware stack has, expressed as
8
+ * two lists rather than as recursion, because two lists are what a Service
9
+ * reads back when it asks "which hooks ran, and in what order".
10
+ *
11
+ * ## A hook cannot fail a request unless it says so
12
+ *
13
+ * Every phase returns a `Result`. An `err` is logged with the hook's name and
14
+ * the phase, and the request carries on. That is the default because a hook is
15
+ * observation: it logs, it annotates, it looks something up. A Service whose
16
+ * log shipper is down should still deliver work. The one exception is a hook
17
+ * that declares `critical: true`, whose `err` replaces the response with the
18
+ * error it returned, at the HTTP status its category maps to. A hook that
19
+ * *throws* is treated exactly as one that returned an `INTERNAL` error, so the
20
+ * distinction between "misbehaved" and "reported a problem" is the `critical`
21
+ * flag and never the control flow.
22
+ *
23
+ * ## What a hook can see and what it can change
24
+ *
25
+ * The context is one object per request, shared by both phases and every hook,
26
+ * with two writable parts. `settlement` is where a hook that looked a Settlement
27
+ * up on Monad attaches what it found (R23.5), and `state` is scratch a hook uses
28
+ * to hand something from its `before` to its `after` phase. A hook
29
+ * cannot replace the request or the response through the context; only a
30
+ * critical failure changes what goes out.
31
+ *
32
+ * Requirements: 23.4, 23.5
33
+ */
34
+ export {};
35
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/proxy/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG","sourcesContent":["/**\n * The hook seam of the proxy layer (R23.4).\n *\n * A hook is a named pair of optional phases. `before` runs ahead of the proxied\n * request, in registration order; `after` runs once the response exists, in\n * reverse order, so the hook registered first is the last to see the response\n * on the way out. That is the same nesting a middleware stack has, expressed as\n * two lists rather than as recursion, because two lists are what a Service\n * reads back when it asks \"which hooks ran, and in what order\".\n *\n * ## A hook cannot fail a request unless it says so\n *\n * Every phase returns a `Result`. An `err` is logged with the hook's name and\n * the phase, and the request carries on. That is the default because a hook is\n * observation: it logs, it annotates, it looks something up. A Service whose\n * log shipper is down should still deliver work. The one exception is a hook\n * that declares `critical: true`, whose `err` replaces the response with the\n * error it returned, at the HTTP status its category maps to. A hook that\n * *throws* is treated exactly as one that returned an `INTERNAL` error, so the\n * distinction between \"misbehaved\" and \"reported a problem\" is the `critical`\n * flag and never the control flow.\n *\n * ## What a hook can see and what it can change\n *\n * The context is one object per request, shared by both phases and every hook,\n * with two writable parts. `settlement` is where a hook that looked a Settlement\n * up on Monad attaches what it found (R23.5), and `state` is scratch a hook uses\n * to hand something from its `before` to its `after` phase. A hook\n * cannot replace the request or the response through the context; only a\n * critical failure changes what goes out.\n *\n * Requirements: 23.4, 23.5\n */\n\nimport type { Result } from \"../_shared/index.js\";\nimport type { Logger } from \"../logger.js\";\nimport type { MeteredCharge, MeteringOutcome } from \"../server/post-paid.js\";\nimport type { Address, Bytes32, Hex } from \"../_shared/index.js\";\n\n/**\n * A Settlement as a hook may attach it to a request: the figures `TabBook`\n * recorded and the Monad transaction that carried them.\n */\nexport interface SettlementView {\n readonly settlementId: Bytes32;\n readonly txHash: Hex;\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: Address;\n readonly amount: bigint;\n readonly applied: bigint;\n readonly toPrepaid: bigint;\n readonly blockNumber?: number;\n}\n\nexport type ProxyPhase = \"before\" | \"after\";\n\nexport interface ProxyHookContext {\n readonly phase: ProxyPhase;\n /** The request as it reached the proxy, before any header was dropped for forwarding. */\n readonly request: Request;\n /** The response about to be returned. Present in the `after` phase only. */\n readonly response?: Response;\n /**\n * The delivery the metering plugin recorded for this request, when it recorded\n * one. Absent when the request was not metered, when metering refused it, and\n * under `release: \"before-metering\"`, where the recording is still pending\n * while the hooks run.\n */\n readonly charge?: MeteredCharge;\n /** The metering outcome in full, present whenever the plugin reached one before the hooks ran. */\n readonly metering?: MeteringOutcome;\n /**\n * The Settlement attached to this request, if a hook found one.\n * Writable: attaching it is a lookup hook's job (R23.5).\n */\n settlement?: SettlementView;\n /** Scratch for the life of one request. Convention: keyed by hook name. */\n readonly state: Map<string, unknown>;\n readonly logger: Logger;\n}\n\nexport interface ProxyHook {\n /** Names the hook in logs and in `state`. Unique within one proxy. */\n readonly name: string;\n /**\n * When true, an `err` from either phase fails the request with that error.\n * Defaults to false: a hook observes, and observation failing is not a reason\n * to withhold delivered work.\n */\n readonly critical?: boolean;\n before?(context: ProxyHookContext): Promise<Result<void>>;\n after?(context: ProxyHookContext): Promise<Result<void>>;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The proxy layer: hooks around a forwarded, metered request (R23.4), with a
3
+ * seam for attaching the Settlement that covers it (R23.5).
4
+ *
5
+ * `hooks.ts` is the seam and `proxy.ts` the handler.
6
+ */
7
+ export * from "./hooks.js";
8
+ export * from "./proxy.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The proxy layer: hooks around a forwarded, metered request (R23.4), with a
3
+ * seam for attaching the Settlement that covers it (R23.5).
4
+ *
5
+ * `hooks.ts` is the seam and `proxy.ts` the handler.
6
+ */
7
+ export * from "./hooks.js";
8
+ export * from "./proxy.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC","sourcesContent":["/**\n * The proxy layer: hooks around a forwarded, metered request (R23.4), with a\n * seam for attaching the Settlement that covers it (R23.5).\n *\n * `hooks.ts` is the seam and `proxy.ts` the handler.\n */\n\nexport * from \"./hooks.js\";\nexport * from \"./proxy.js\";\n"]}