@hanzo/commerce 7.6.0 → 7.6.3

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 (258) hide show
  1. package/LICENSE.md +21 -0
  2. package/client.ts +155 -39
  3. package/dist/billing.d.ts +2 -0
  4. package/dist/billing.js +3 -0
  5. package/dist/billing.js.map +1 -0
  6. package/dist/client.d.ts +464 -0
  7. package/dist/client.js +424 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/components/Icons.d.ts +18 -0
  10. package/dist/components/Icons.js +19 -0
  11. package/dist/components/Icons.js.map +1 -0
  12. package/dist/components/add-to-cart-widget.d.ts +11 -0
  13. package/dist/components/add-to-cart-widget.js +85 -0
  14. package/dist/components/add-to-cart-widget.js.map +1 -0
  15. package/dist/components/buy/buy-card.d.ts +30 -0
  16. package/dist/components/buy/buy-card.js +109 -0
  17. package/dist/components/buy/buy-card.js.map +1 -0
  18. package/dist/components/buy/carousel-buy-card.d.ts +12 -0
  19. package/dist/components/buy/carousel-buy-card.js +94 -0
  20. package/dist/components/buy/carousel-buy-card.js.map +1 -0
  21. package/dist/components/buy/multi-family/all-variants-carousel.d.ts +4 -0
  22. package/dist/components/buy/multi-family/all-variants-carousel.js +115 -0
  23. package/dist/components/buy/multi-family/all-variants-carousel.js.map +1 -0
  24. package/dist/components/buy/multi-family/family-carousel/index.d.ts +4 -0
  25. package/dist/components/buy/multi-family/family-carousel/index.js +27 -0
  26. package/dist/components/buy/multi-family/family-carousel/index.js.map +1 -0
  27. package/dist/components/buy/multi-family/family-carousel/slide.d.ts +11 -0
  28. package/dist/components/buy/multi-family/family-carousel/slide.js +35 -0
  29. package/dist/components/buy/multi-family/family-carousel/slide.js.map +1 -0
  30. package/dist/components/buy/multi-family/family-carousel/state.d.ts +20 -0
  31. package/dist/components/buy/multi-family/family-carousel/state.js +59 -0
  32. package/dist/components/buy/multi-family/family-carousel/state.js.map +1 -0
  33. package/dist/components/buy/multi-family/index.d.ts +2 -0
  34. package/dist/components/buy/multi-family/index.js +3 -0
  35. package/dist/components/buy/multi-family/index.js.map +1 -0
  36. package/dist/components/buy/single-family-selector.d.ts +15 -0
  37. package/dist/components/buy/single-family-selector.js +28 -0
  38. package/dist/components/buy/single-family-selector.js.map +1 -0
  39. package/dist/components/buy/title-and-byline.d.ts +8 -0
  40. package/dist/components/buy/title-and-byline.js +7 -0
  41. package/dist/components/buy/title-and-byline.js.map +1 -0
  42. package/dist/components/cart/cart-panel/cart-line-item.d.ts +11 -0
  43. package/dist/components/cart/cart-panel/cart-line-item.js +25 -0
  44. package/dist/components/cart/cart-panel/cart-line-item.js.map +1 -0
  45. package/dist/components/cart/cart-panel/index.d.ts +19 -0
  46. package/dist/components/cart/cart-panel/index.js +65 -0
  47. package/dist/components/cart/cart-panel/index.js.map +1 -0
  48. package/dist/components/cart/cart-panel/promo-code.d.ts +4 -0
  49. package/dist/components/cart/cart-panel/promo-code.js +62 -0
  50. package/dist/components/cart/cart-panel/promo-code.js.map +1 -0
  51. package/dist/components/cart/cart-panel/total-area.d.ts +7 -0
  52. package/dist/components/cart/cart-panel/total-area.js +14 -0
  53. package/dist/components/cart/cart-panel/total-area.js.map +1 -0
  54. package/dist/components/checkout/payment-step-form/card-icon-row.d.ts +2 -0
  55. package/dist/components/checkout/payment-step-form/card-icon-row.js +14 -0
  56. package/dist/components/checkout/payment-step-form/card-icon-row.js.map +1 -0
  57. package/dist/components/checkout/payment-step-form/card-icons/amex.d.ts +4 -0
  58. package/dist/components/checkout/payment-step-form/card-icons/amex.js +6 -0
  59. package/dist/components/checkout/payment-step-form/card-icons/amex.js.map +1 -0
  60. package/dist/components/checkout/payment-step-form/card-icons/diners-club.d.ts +4 -0
  61. package/dist/components/checkout/payment-step-form/card-icons/diners-club.js +6 -0
  62. package/dist/components/checkout/payment-step-form/card-icons/diners-club.js.map +1 -0
  63. package/dist/components/checkout/payment-step-form/card-icons/discover.d.ts +4 -0
  64. package/dist/components/checkout/payment-step-form/card-icons/discover.js +6 -0
  65. package/dist/components/checkout/payment-step-form/card-icons/discover.js.map +1 -0
  66. package/dist/components/checkout/payment-step-form/card-icons/jcb.d.ts +4 -0
  67. package/dist/components/checkout/payment-step-form/card-icons/jcb.js +6 -0
  68. package/dist/components/checkout/payment-step-form/card-icons/jcb.js.map +1 -0
  69. package/dist/components/checkout/payment-step-form/card-icons/mastercard.d.ts +4 -0
  70. package/dist/components/checkout/payment-step-form/card-icons/mastercard.js +6 -0
  71. package/dist/components/checkout/payment-step-form/card-icons/mastercard.js.map +1 -0
  72. package/dist/components/checkout/payment-step-form/card-icons/visa.d.ts +4 -0
  73. package/dist/components/checkout/payment-step-form/card-icons/visa.js +6 -0
  74. package/dist/components/checkout/payment-step-form/card-icons/visa.js.map +1 -0
  75. package/dist/components/checkout/payment-step-form/cc-button.d.ts +3 -0
  76. package/dist/components/checkout/payment-step-form/cc-button.js +6 -0
  77. package/dist/components/checkout/payment-step-form/cc-button.js.map +1 -0
  78. package/dist/components/checkout/payment-step-form/contact-form.d.ts +5 -0
  79. package/dist/components/checkout/payment-step-form/contact-form.js +6 -0
  80. package/dist/components/checkout/payment-step-form/contact-form.js.map +1 -0
  81. package/dist/components/checkout/payment-step-form/crypto-icons/btc.d.ts +4 -0
  82. package/dist/components/checkout/payment-step-form/crypto-icons/btc.js +6 -0
  83. package/dist/components/checkout/payment-step-form/crypto-icons/btc.js.map +1 -0
  84. package/dist/components/checkout/payment-step-form/crypto-icons/eth.d.ts +4 -0
  85. package/dist/components/checkout/payment-step-form/crypto-icons/eth.js +6 -0
  86. package/dist/components/checkout/payment-step-form/crypto-icons/eth.js.map +1 -0
  87. package/dist/components/checkout/payment-step-form/crypto-icons/usdt.d.ts +4 -0
  88. package/dist/components/checkout/payment-step-form/crypto-icons/usdt.js +6 -0
  89. package/dist/components/checkout/payment-step-form/crypto-icons/usdt.js.map +1 -0
  90. package/dist/components/checkout/payment-step-form/index.d.ts +4 -0
  91. package/dist/components/checkout/payment-step-form/index.js +66 -0
  92. package/dist/components/checkout/payment-step-form/index.js.map +1 -0
  93. package/dist/components/checkout/payment-step-form/methods/bank-transfer.d.ts +4 -0
  94. package/dist/components/checkout/payment-step-form/methods/bank-transfer.js +24 -0
  95. package/dist/components/checkout/payment-step-form/methods/bank-transfer.js.map +1 -0
  96. package/dist/components/checkout/payment-step-form/methods/card.d.ts +4 -0
  97. package/dist/components/checkout/payment-step-form/methods/card.js +160 -0
  98. package/dist/components/checkout/payment-step-form/methods/card.js.map +1 -0
  99. package/dist/components/checkout/payment-step-form/methods/crypto.d.ts +9 -0
  100. package/dist/components/checkout/payment-step-form/methods/crypto.js +137 -0
  101. package/dist/components/checkout/payment-step-form/methods/crypto.js.map +1 -0
  102. package/dist/components/checkout/payment-step-form/methods/index.d.ts +6 -0
  103. package/dist/components/checkout/payment-step-form/methods/index.js +21 -0
  104. package/dist/components/checkout/payment-step-form/methods/index.js.map +1 -0
  105. package/dist/components/checkout/shipping-step-form.d.ts +3 -0
  106. package/dist/components/checkout/shipping-step-form.js +53 -0
  107. package/dist/components/checkout/shipping-step-form.js.map +1 -0
  108. package/dist/components/index.d.ts +8 -0
  109. package/dist/components/index.js +9 -0
  110. package/dist/components/index.js.map +1 -0
  111. package/dist/components/item/product-card.d.ts +7 -0
  112. package/dist/components/item/product-card.js +9 -0
  113. package/dist/components/item/product-card.js.map +1 -0
  114. package/dist/components/item-selector/button.d.ts +4 -0
  115. package/dist/components/item-selector/button.js +47 -0
  116. package/dist/components/item-selector/button.js.map +1 -0
  117. package/dist/components/item-selector/carousel/index.d.ts +12 -0
  118. package/dist/components/item-selector/carousel/index.js +74 -0
  119. package/dist/components/item-selector/carousel/index.js.map +1 -0
  120. package/dist/components/item-selector/carousel/slider.d.ts +8 -0
  121. package/dist/components/item-selector/carousel/slider.js +12 -0
  122. package/dist/components/item-selector/carousel/slider.js.map +1 -0
  123. package/dist/components/item-selector/index.d.ts +2 -0
  124. package/dist/components/item-selector/index.js +3 -0
  125. package/dist/components/item-selector/index.js.map +1 -0
  126. package/dist/components/item-selector/quantity-indicator.d.ts +9 -0
  127. package/dist/components/item-selector/quantity-indicator.js +16 -0
  128. package/dist/components/item-selector/quantity-indicator.js.map +1 -0
  129. package/dist/components/node-tabs/index.d.ts +14 -0
  130. package/dist/components/node-tabs/index.js +42 -0
  131. package/dist/components/node-tabs/index.js.map +1 -0
  132. package/dist/components/node-tabs/node-image.d.ts +6 -0
  133. package/dist/components/node-tabs/node-image.js +13 -0
  134. package/dist/components/node-tabs/node-image.js.map +1 -0
  135. package/dist/index.d.ts +9 -0
  136. package/dist/index.js +9 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/metering.d.ts +162 -0
  139. package/dist/metering.js +204 -0
  140. package/dist/metering.js.map +1 -0
  141. package/dist/metering.test.d.ts +13 -0
  142. package/dist/metering.test.js +181 -0
  143. package/dist/metering.test.js.map +1 -0
  144. package/dist/service/context.d.ts +8 -0
  145. package/dist/service/context.js +19 -0
  146. package/dist/service/context.js.map +1 -0
  147. package/dist/service/debug.d.ts +10 -0
  148. package/dist/service/debug.js +30 -0
  149. package/dist/service/debug.js.map +1 -0
  150. package/dist/service/impls/standalone/actual-line-item.d.ts +40 -0
  151. package/dist/service/impls/standalone/actual-line-item.js +84 -0
  152. package/dist/service/impls/standalone/actual-line-item.js.map +1 -0
  153. package/dist/service/impls/standalone/get-instance.d.ts +2 -0
  154. package/dist/service/impls/standalone/get-instance.js +39 -0
  155. package/dist/service/impls/standalone/get-instance.js.map +1 -0
  156. package/dist/service/impls/standalone/index.d.ts +67 -0
  157. package/dist/service/impls/standalone/index.js +416 -0
  158. package/dist/service/impls/standalone/index.js.map +1 -0
  159. package/dist/service/impls/standalone/order/firebase.d.ts +6 -0
  160. package/dist/service/impls/standalone/order/firebase.js +6 -0
  161. package/dist/service/impls/standalone/order/firebase.js.map +1 -0
  162. package/dist/service/impls/standalone/order/index.d.ts +30 -0
  163. package/dist/service/impls/standalone/order/index.js +21 -0
  164. package/dist/service/impls/standalone/order/index.js.map +1 -0
  165. package/dist/service/impls/standalone/persistence.d.ts +4 -0
  166. package/dist/service/impls/standalone/persistence.js +22 -0
  167. package/dist/service/impls/standalone/persistence.js.map +1 -0
  168. package/dist/service/path-utils.d.ts +7 -0
  169. package/dist/service/path-utils.js +16 -0
  170. package/dist/service/path-utils.js.map +1 -0
  171. package/dist/service/sep.d.ts +6 -0
  172. package/dist/service/sep.js +6 -0
  173. package/dist/service/sep.js.map +1 -0
  174. package/dist/types/category-node.d.ts +36 -0
  175. package/dist/types/category-node.js +2 -0
  176. package/dist/types/category-node.js.map +1 -0
  177. package/dist/types/checkout.d.ts +33 -0
  178. package/dist/types/checkout.js +2 -0
  179. package/dist/types/checkout.js.map +1 -0
  180. package/dist/types/commerce-config.d.ts +11 -0
  181. package/dist/types/commerce-config.js +2 -0
  182. package/dist/types/commerce-config.js.map +1 -0
  183. package/dist/types/commerce-service.d.ts +109 -0
  184. package/dist/types/commerce-service.js +2 -0
  185. package/dist/types/commerce-service.js.map +1 -0
  186. package/dist/types/family.d.ts +16 -0
  187. package/dist/types/family.js +2 -0
  188. package/dist/types/family.js.map +1 -0
  189. package/dist/types/index.d.ts +13 -0
  190. package/dist/types/index.js +8 -0
  191. package/dist/types/index.js.map +1 -0
  192. package/dist/types/item-selector.d.ts +72 -0
  193. package/dist/types/item-selector.js +2 -0
  194. package/dist/types/item-selector.js.map +1 -0
  195. package/dist/types/line-item.d.ts +14 -0
  196. package/dist/types/line-item.js +2 -0
  197. package/dist/types/line-item.js.map +1 -0
  198. package/dist/types/multi-family-selector-props.d.ts +16 -0
  199. package/dist/types/multi-family-selector-props.js +2 -0
  200. package/dist/types/multi-family-selector-props.js.map +1 -0
  201. package/dist/types/product.d.ts +15 -0
  202. package/dist/types/product.js +2 -0
  203. package/dist/types/product.js.map +1 -0
  204. package/dist/types/promo.d.ts +7 -0
  205. package/dist/types/promo.js +2 -0
  206. package/dist/types/promo.js.map +1 -0
  207. package/dist/types/selection-ui-specifier.d.ts +40 -0
  208. package/dist/types/selection-ui-specifier.js +2 -0
  209. package/dist/types/selection-ui-specifier.js.map +1 -0
  210. package/dist/types/string-mutator.d.ts +9 -0
  211. package/dist/types/string-mutator.js +2 -0
  212. package/dist/types/string-mutator.js.map +1 -0
  213. package/dist/types/token-separators.d.ts +6 -0
  214. package/dist/types/token-separators.js +2 -0
  215. package/dist/types/token-separators.js.map +1 -0
  216. package/dist/util/analytics.d.ts +9 -0
  217. package/dist/util/analytics.js +10 -0
  218. package/dist/util/analytics.js.map +1 -0
  219. package/dist/util/countries.d.ts +7 -0
  220. package/dist/util/countries.js +197 -0
  221. package/dist/util/countries.js.map +1 -0
  222. package/dist/util/error.d.ts +1 -0
  223. package/dist/util/error.js +22 -0
  224. package/dist/util/error.js.map +1 -0
  225. package/dist/util/index.d.ts +15 -0
  226. package/dist/util/index.js +54 -0
  227. package/dist/util/index.js.map +1 -0
  228. package/dist/util/item-selector-options-accessor.d.ts +3 -0
  229. package/dist/util/item-selector-options-accessor.js +27 -0
  230. package/dist/util/item-selector-options-accessor.js.map +1 -0
  231. package/dist/util/line-item-ref.d.ts +8 -0
  232. package/dist/util/line-item-ref.js +15 -0
  233. package/dist/util/line-item-ref.js.map +1 -0
  234. package/dist/util/multi-family-selector-options-accessor.d.ts +3 -0
  235. package/dist/util/multi-family-selector-options-accessor.js +11 -0
  236. package/dist/util/multi-family-selector-options-accessor.js.map +1 -0
  237. package/dist/util/obs-string-mutator.d.ts +8 -0
  238. package/dist/util/obs-string-mutator.js +15 -0
  239. package/dist/util/obs-string-mutator.js.map +1 -0
  240. package/dist/util/product-media-accessor.d.ts +29 -0
  241. package/dist/util/product-media-accessor.js +22 -0
  242. package/dist/util/product-media-accessor.js.map +1 -0
  243. package/dist/util/promo-codes.d.ts +3 -0
  244. package/dist/util/promo-codes.js +100 -0
  245. package/dist/util/promo-codes.js.map +1 -0
  246. package/dist/util/selection-ui-specifiers.d.ts +3 -0
  247. package/dist/util/selection-ui-specifiers.js +24 -0
  248. package/dist/util/selection-ui-specifiers.js.map +1 -0
  249. package/dist/util/square-payment.d.ts +7 -0
  250. package/dist/util/square-payment.js +37 -0
  251. package/dist/util/square-payment.js.map +1 -0
  252. package/dist/util/use-sync-sku-param-w-current-item.d.ts +2 -0
  253. package/dist/util/use-sync-sku-param-w-current-item.js +61 -0
  254. package/dist/util/use-sync-sku-param-w-current-item.js.map +1 -0
  255. package/index.ts +18 -1
  256. package/metering.test.ts +199 -0
  257. package/metering.ts +289 -0
  258. package/package.json +18 -16
package/metering.ts ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @hanzo/commerce/metering
3
+ *
4
+ * The ONE way every Hanzo product meters usage to commerce — the single
5
+ * billing source of truth — so that everything (not only the LLM/cloud path)
6
+ * can be paid for. This is the TypeScript counterpart of
7
+ * github.com/hanzoai/go-sdk/metering; the two share an identical wire contract.
8
+ *
9
+ * It provides two operations against commerce's canonical billing API:
10
+ *
11
+ * - authorize(): a pre-request balance gate. Fail-closed by default — if the
12
+ * balance cannot be determined the request is denied, exactly like the
13
+ * gateway's prepaid-balance gate. With tierAware it consults the effective
14
+ * balance (prepaid + included plan allotment, e.g. the free-tier daily
15
+ * credit) so included usage is honored before prepaid funds.
16
+ *
17
+ * - record(): a post-request usage write that debits the user's balance.
18
+ *
19
+ * Composition over duplication: this wraps the existing {@link Commerce}
20
+ * client (GET /v1/billing/balance, GET /v1/billing/tier, POST /v1/billing/usage)
21
+ * rather than re-implementing the HTTP layer.
22
+ *
23
+ * Auth is the commerce service token (admin-scoped S2S), sent as
24
+ * `Authorization: Bearer <token>` plus the tenant org as `X-IAM-Org-Id`. The
25
+ * token is a secret and MUST come from KMS (the operator wires it into
26
+ * COMMERCE_SERVICE_TOKEN); this module never reads it from disk.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { Metering } from '@hanzo/commerce/metering'
31
+ *
32
+ * const meter = Metering.fromEnv() // COMMERCE_URL + COMMERCE_SERVICE_TOKEN (KMS) + COMMERCE_SERVICE_ORG
33
+ *
34
+ * // Pre-request gate (fail-closed).
35
+ * const gate = await meter.authorize({ user: 'hanzo/alice' })
36
+ * if (!gate.allowed) {
37
+ * res.status(gate.reason === 'insufficient_balance' ? 402 : 503).end()
38
+ * return
39
+ * }
40
+ *
41
+ * // ... do the work, compute cost ...
42
+ *
43
+ * // Post-request record (best-effort).
44
+ * await meter.record({ user: 'hanzo/alice', amount: 5, provider: 'search' })
45
+ * ```
46
+ */
47
+
48
+ import { Commerce, CommerceApiError } from './client'
49
+
50
+ /** Canonical in-cluster commerce address (matches the gateway default). */
51
+ export const DEFAULT_COMMERCE_URL = 'http://commerce.hanzo.svc.cluster.local:8001'
52
+
53
+ export type MeteringConfig = {
54
+ /**
55
+ * Commerce base URL (no /v1 suffix). When empty the meter is in
56
+ * "not configured" mode: authorize allows and record is a no-op.
57
+ */
58
+ baseUrl?: string
59
+ /** Commerce service token (admin-scoped S2S). MUST be KMS-sourced. */
60
+ token?: string
61
+ /** Default tenant org slug sent as X-IAM-Org-Id (default "hanzo"). */
62
+ org?: string
63
+ /**
64
+ * Gate on the tier-aware effective balance (prepaid + included plan
65
+ * allotment) instead of the bare prepaid balance. Default false.
66
+ */
67
+ tierAware?: boolean
68
+ /**
69
+ * Allow-on-error. Default false (fail-closed, like the gateway). Set true
70
+ * only where availability outranks billing.
71
+ */
72
+ failOpen?: boolean
73
+ /** Request timeout in ms. Default 5000 (the gateway's value). */
74
+ timeoutMs?: number
75
+ }
76
+
77
+ /** Who to authorize / charge. user is the IAM "org/sub" identity. */
78
+ export type MeteringIdentity = {
79
+ user: string
80
+ org?: string
81
+ currency?: string
82
+ }
83
+
84
+ /** One usage event to record. Mirrors commerce's usageRequest one-for-one. */
85
+ export type UsageEvent = {
86
+ user: string
87
+ org?: string
88
+ /** Cost to debit, in cents. */
89
+ amount: number
90
+ currency?: string
91
+ model?: string
92
+ provider?: string
93
+ promptTokens?: number
94
+ completionTokens?: number
95
+ totalTokens?: number
96
+ requestId?: string
97
+ premium?: boolean
98
+ stream?: boolean
99
+ status?: string
100
+ clientIp?: string
101
+ }
102
+
103
+ /** Result of a usage write. */
104
+ export type RecordResult = {
105
+ transactionId: string
106
+ user: string
107
+ amount: number
108
+ currency: string
109
+ type: string
110
+ }
111
+
112
+ /** Outcome of an authorize() gate. */
113
+ export type AuthDecision = {
114
+ allowed: boolean
115
+ /** 'insufficient_balance' -> 402 ; 'balance_unavailable' -> 503 (fail-closed). */
116
+ reason?: 'insufficient_balance' | 'balance_unavailable'
117
+ /** Spendable balance in cents that the decision was based on (when known). */
118
+ available?: number
119
+ }
120
+
121
+ /**
122
+ * Metering is the server-side hook a product uses to charge for usage. It is
123
+ * safe to share a single instance across requests.
124
+ */
125
+ export class Metering {
126
+ private readonly commerce: Commerce | null
127
+ private readonly org: string
128
+ private readonly tierAware: boolean
129
+ private readonly failOpen: boolean
130
+
131
+ constructor(config: MeteringConfig = {}) {
132
+ const baseUrl = (config.baseUrl ?? '').replace(/\/+$/, '')
133
+ this.commerce = baseUrl
134
+ ? new Commerce({ baseUrl, token: config.token, timeoutMs: config.timeoutMs ?? 5000 })
135
+ : null
136
+ this.org = (config.org ?? '').trim()
137
+ this.tierAware = config.tierAware ?? false
138
+ this.failOpen = config.failOpen ?? false
139
+ }
140
+
141
+ /**
142
+ * Build a Metering instance from the canonical environment variables. This
143
+ * is the one-liner products use at startup. Honors METERING_DISABLED for
144
+ * local dev.
145
+ */
146
+ static fromEnv(env: Record<string, string | undefined> = readEnv()): Metering {
147
+ if (truthy(env.METERING_DISABLED)) return new Metering({})
148
+ return new Metering({
149
+ baseUrl: (env.COMMERCE_URL ?? '').trim() || DEFAULT_COMMERCE_URL,
150
+ token: env.COMMERCE_SERVICE_TOKEN,
151
+ org: (env.COMMERCE_SERVICE_ORG ?? '').trim() || 'hanzo',
152
+ tierAware: truthy(env.METERING_TIER_AWARE),
153
+ failOpen: truthy(env.METERING_FAIL_OPEN),
154
+ })
155
+ }
156
+
157
+ /** Whether a commerce base URL is configured. */
158
+ get enabled(): boolean {
159
+ return this.commerce !== null
160
+ }
161
+
162
+ /**
163
+ * Pre-request balance gate. Never throws — returns a decision so callers map
164
+ * it to a status code. When not configured it always allows.
165
+ *
166
+ * Outcomes (matching the gateway):
167
+ * { allowed: true } -> allow.
168
+ * { allowed: false, reason: 'insufficient_balance' } -> 402 (out of funds).
169
+ * { allowed: false, reason: 'balance_unavailable' } -> 503 (fail-closed).
170
+ */
171
+ async authorize(id: MeteringIdentity): Promise<AuthDecision> {
172
+ if (!this.commerce) return { allowed: true }
173
+
174
+ const user = (id.user ?? '').trim()
175
+ if (!user) {
176
+ return this.failOpen ? { allowed: true } : { allowed: false, reason: 'balance_unavailable' }
177
+ }
178
+
179
+ const headers = orgHeader(id.org ?? this.org)
180
+ try {
181
+ const available = this.tierAware
182
+ ? (await this.commerce.getTier(user, { headers })).balance.effectiveAvailable
183
+ : (await this.commerce.getBalance(user, id.currency ?? 'usd', undefined, headers)).available
184
+
185
+ if (available > 0) return { allowed: true, available }
186
+ return { allowed: false, reason: 'insufficient_balance', available }
187
+ } catch {
188
+ // Balance unknown (commerce unreachable / non-2xx).
189
+ if (this.failOpen) return { allowed: true }
190
+ return { allowed: false, reason: 'balance_unavailable' }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Record a usage event, debiting the user's balance. No-op when not
196
+ * configured or amount <= 0 (commerce treats zero-cost usage as skipped).
197
+ * Decoupled from gating: the work already happened, so balance is not
198
+ * re-checked here. Returns null on no-op.
199
+ */
200
+ async record(usage: UsageEvent): Promise<RecordResult | null> {
201
+ if (!this.commerce || usage.amount <= 0) return null
202
+ if (!usage.user?.trim()) throw new Error('metering: record requires a user')
203
+
204
+ const headers = orgHeader(usage.org ?? this.org)
205
+ // Commerce's usageRequest field names map one-for-one; org travels via the
206
+ // header, never the body.
207
+ const body = {
208
+ user: usage.user,
209
+ currency: usage.currency ?? 'usd',
210
+ amount: usage.amount,
211
+ model: usage.model,
212
+ provider: usage.provider,
213
+ promptTokens: usage.promptTokens,
214
+ completionTokens: usage.completionTokens,
215
+ totalTokens: usage.totalTokens,
216
+ requestId: usage.requestId,
217
+ premium: usage.premium,
218
+ stream: usage.stream,
219
+ status: usage.status,
220
+ clientIp: usage.clientIp,
221
+ }
222
+ const tx = await this.commerce.addUsageRecord(body as never, undefined, headers)
223
+ // Commerce returns { transactionId, user, amount, currency, type }.
224
+ return tx as unknown as RecordResult
225
+ }
226
+
227
+ /**
228
+ * Convenience for products that prefer to throw on denial. Throws a
229
+ * {@link CommerceApiError} with status 402 (insufficient) or 503 (unknown).
230
+ */
231
+ async authorizeOrThrow(id: MeteringIdentity): Promise<void> {
232
+ const decision = await this.authorize(id)
233
+ if (decision.allowed) return
234
+ if (decision.reason === 'insufficient_balance') {
235
+ throw new CommerceApiError(402, 'Insufficient balance. Please add credits at console.hanzo.ai')
236
+ }
237
+ throw new CommerceApiError(503, 'Billing temporarily unavailable')
238
+ }
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Identity from gateway-minted headers (the trust boundary)
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /** Gateway-minted identity headers. */
246
+ export const HEADER_USER_ID = 'x-user-id'
247
+ export const HEADER_ORG_ID = 'x-org-id'
248
+
249
+ /**
250
+ * Build a {@link MeteringIdentity} from gateway-minted identity headers. The
251
+ * user is "{org}/{sub}" (commerce's iam-user form) when both are present.
252
+ * Accepts a header bag with string | string[] | undefined values (Node http,
253
+ * Next.js, fetch Headers via Object.fromEntries).
254
+ */
255
+ export function identityFromHeaders(
256
+ headers: Record<string, string | string[] | undefined> | Headers,
257
+ ): MeteringIdentity {
258
+ const get = (k: string): string => {
259
+ if (typeof (headers as Headers).get === 'function') {
260
+ return ((headers as Headers).get(k) ?? '').trim()
261
+ }
262
+ const v = (headers as Record<string, string | string[] | undefined>)[k]
263
+ return (Array.isArray(v) ? v[0] : v ?? '').trim()
264
+ }
265
+ const org = get(HEADER_ORG_ID)
266
+ const sub = get(HEADER_USER_ID)
267
+ const user = org && sub ? `${org}/${sub}` : sub
268
+ return { user, org }
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // internals
273
+ // ---------------------------------------------------------------------------
274
+
275
+ function orgHeader(org: string): Record<string, string> | undefined {
276
+ const o = (org ?? '').trim()
277
+ return o ? { 'X-IAM-Org-Id': o } : undefined
278
+ }
279
+
280
+ function truthy(v: string | undefined): boolean {
281
+ const s = (v ?? '').trim().toLowerCase()
282
+ return s === 'true' || s === '1' || s === 'yes'
283
+ }
284
+
285
+ function readEnv(): Record<string, string | undefined> {
286
+ // Node/edge: process.env when present, else empty (browser).
287
+ const g = globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }
288
+ return g.process?.env ?? {}
289
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/commerce",
3
- "version": "7.6.0",
3
+ "version": "7.6.3",
4
4
  "description": "e-commerce framework.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/",
@@ -12,32 +12,27 @@
12
12
  "repository": {
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/hanzoai/ui.git",
15
- "directory": "pkg/commerce"
15
+ "directory": "pkgs/commerce"
16
16
  },
17
17
  "keywords": [
18
18
  "cart",
19
19
  "hanzoai",
20
20
  "hanzo"
21
21
  ],
22
- "scripts": {
23
- "lat": "npm show @hanzo/commerce version",
24
- "pub": "npm publish",
25
- "tc": "tsc",
26
- "build": "tsc --outDir dist --declaration --emitDeclarationOnly false --module ESNext --esModuleInterop true --skipLibCheck true --sourceMap true"
27
- },
28
22
  "exports": {
29
23
  ".": "./index.ts",
30
24
  "./types": "./types/index.ts",
31
25
  "./client": "./client.ts",
32
26
  "./billing": "./billing.ts",
27
+ "./metering": "./metering.ts",
33
28
  "./debug": "./service/debug.ts"
34
29
  },
35
30
  "dependencies": {
36
- "ethers": "^6.12.0",
37
- "next-usequerystate": "^1.17.1"
31
+ "ethers": "^6.16.0",
32
+ "next-usequerystate": "^1.20.0"
38
33
  },
39
34
  "peerDependencies": {
40
- "@hanzo/ui": ">=5.0.0",
35
+ "@hanzo/ui": "npm:@hanzo/ui-shadcn@^5",
41
36
  "@hookform/resolvers": "^3.3.4",
42
37
  "@radix-ui/react-radio-group": "^1.1.3",
43
38
  "lucide-react": ">=0.456.0",
@@ -51,9 +46,16 @@
51
46
  "zod": "^3.23.8"
52
47
  },
53
48
  "devDependencies": {
54
- "@types/react": "18.3.12",
55
- "@types/react-dom": "18.3.1",
56
- "cross-fetch": "^4.0.0",
57
- "typescript": "5.6.3"
49
+ "@types/react": "19.2.14",
50
+ "@types/react-dom": "19.2.3",
51
+ "cross-fetch": "^4.1.0",
52
+ "typescript": "^5.9.3",
53
+ "@hanzo/ui": "npm:@hanzo/ui-shadcn@^5.9.1"
54
+ },
55
+ "scripts": {
56
+ "lat": "npm show @hanzo/commerce version",
57
+ "pub": "npm publish",
58
+ "tc": "tsc",
59
+ "build": "tsc --outDir dist --declaration --emitDeclarationOnly false --module ESNext --esModuleInterop true --skipLibCheck true --sourceMap true"
58
60
  }
59
- }
61
+ }