@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.
- package/LICENSE.md +21 -0
- package/client.ts +155 -39
- package/dist/billing.d.ts +2 -0
- package/dist/billing.js +3 -0
- package/dist/billing.js.map +1 -0
- package/dist/client.d.ts +464 -0
- package/dist/client.js +424 -0
- package/dist/client.js.map +1 -0
- package/dist/components/Icons.d.ts +18 -0
- package/dist/components/Icons.js +19 -0
- package/dist/components/Icons.js.map +1 -0
- package/dist/components/add-to-cart-widget.d.ts +11 -0
- package/dist/components/add-to-cart-widget.js +85 -0
- package/dist/components/add-to-cart-widget.js.map +1 -0
- package/dist/components/buy/buy-card.d.ts +30 -0
- package/dist/components/buy/buy-card.js +109 -0
- package/dist/components/buy/buy-card.js.map +1 -0
- package/dist/components/buy/carousel-buy-card.d.ts +12 -0
- package/dist/components/buy/carousel-buy-card.js +94 -0
- package/dist/components/buy/carousel-buy-card.js.map +1 -0
- package/dist/components/buy/multi-family/all-variants-carousel.d.ts +4 -0
- package/dist/components/buy/multi-family/all-variants-carousel.js +115 -0
- package/dist/components/buy/multi-family/all-variants-carousel.js.map +1 -0
- package/dist/components/buy/multi-family/family-carousel/index.d.ts +4 -0
- package/dist/components/buy/multi-family/family-carousel/index.js +27 -0
- package/dist/components/buy/multi-family/family-carousel/index.js.map +1 -0
- package/dist/components/buy/multi-family/family-carousel/slide.d.ts +11 -0
- package/dist/components/buy/multi-family/family-carousel/slide.js +35 -0
- package/dist/components/buy/multi-family/family-carousel/slide.js.map +1 -0
- package/dist/components/buy/multi-family/family-carousel/state.d.ts +20 -0
- package/dist/components/buy/multi-family/family-carousel/state.js +59 -0
- package/dist/components/buy/multi-family/family-carousel/state.js.map +1 -0
- package/dist/components/buy/multi-family/index.d.ts +2 -0
- package/dist/components/buy/multi-family/index.js +3 -0
- package/dist/components/buy/multi-family/index.js.map +1 -0
- package/dist/components/buy/single-family-selector.d.ts +15 -0
- package/dist/components/buy/single-family-selector.js +28 -0
- package/dist/components/buy/single-family-selector.js.map +1 -0
- package/dist/components/buy/title-and-byline.d.ts +8 -0
- package/dist/components/buy/title-and-byline.js +7 -0
- package/dist/components/buy/title-and-byline.js.map +1 -0
- package/dist/components/cart/cart-panel/cart-line-item.d.ts +11 -0
- package/dist/components/cart/cart-panel/cart-line-item.js +25 -0
- package/dist/components/cart/cart-panel/cart-line-item.js.map +1 -0
- package/dist/components/cart/cart-panel/index.d.ts +19 -0
- package/dist/components/cart/cart-panel/index.js +65 -0
- package/dist/components/cart/cart-panel/index.js.map +1 -0
- package/dist/components/cart/cart-panel/promo-code.d.ts +4 -0
- package/dist/components/cart/cart-panel/promo-code.js +62 -0
- package/dist/components/cart/cart-panel/promo-code.js.map +1 -0
- package/dist/components/cart/cart-panel/total-area.d.ts +7 -0
- package/dist/components/cart/cart-panel/total-area.js +14 -0
- package/dist/components/cart/cart-panel/total-area.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icon-row.d.ts +2 -0
- package/dist/components/checkout/payment-step-form/card-icon-row.js +14 -0
- package/dist/components/checkout/payment-step-form/card-icon-row.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icons/amex.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/card-icons/amex.js +6 -0
- package/dist/components/checkout/payment-step-form/card-icons/amex.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icons/diners-club.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/card-icons/diners-club.js +6 -0
- package/dist/components/checkout/payment-step-form/card-icons/diners-club.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icons/discover.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/card-icons/discover.js +6 -0
- package/dist/components/checkout/payment-step-form/card-icons/discover.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icons/jcb.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/card-icons/jcb.js +6 -0
- package/dist/components/checkout/payment-step-form/card-icons/jcb.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icons/mastercard.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/card-icons/mastercard.js +6 -0
- package/dist/components/checkout/payment-step-form/card-icons/mastercard.js.map +1 -0
- package/dist/components/checkout/payment-step-form/card-icons/visa.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/card-icons/visa.js +6 -0
- package/dist/components/checkout/payment-step-form/card-icons/visa.js.map +1 -0
- package/dist/components/checkout/payment-step-form/cc-button.d.ts +3 -0
- package/dist/components/checkout/payment-step-form/cc-button.js +6 -0
- package/dist/components/checkout/payment-step-form/cc-button.js.map +1 -0
- package/dist/components/checkout/payment-step-form/contact-form.d.ts +5 -0
- package/dist/components/checkout/payment-step-form/contact-form.js +6 -0
- package/dist/components/checkout/payment-step-form/contact-form.js.map +1 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/btc.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/btc.js +6 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/btc.js.map +1 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/eth.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/eth.js +6 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/eth.js.map +1 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/usdt.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/usdt.js +6 -0
- package/dist/components/checkout/payment-step-form/crypto-icons/usdt.js.map +1 -0
- package/dist/components/checkout/payment-step-form/index.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/index.js +66 -0
- package/dist/components/checkout/payment-step-form/index.js.map +1 -0
- package/dist/components/checkout/payment-step-form/methods/bank-transfer.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/methods/bank-transfer.js +24 -0
- package/dist/components/checkout/payment-step-form/methods/bank-transfer.js.map +1 -0
- package/dist/components/checkout/payment-step-form/methods/card.d.ts +4 -0
- package/dist/components/checkout/payment-step-form/methods/card.js +160 -0
- package/dist/components/checkout/payment-step-form/methods/card.js.map +1 -0
- package/dist/components/checkout/payment-step-form/methods/crypto.d.ts +9 -0
- package/dist/components/checkout/payment-step-form/methods/crypto.js +137 -0
- package/dist/components/checkout/payment-step-form/methods/crypto.js.map +1 -0
- package/dist/components/checkout/payment-step-form/methods/index.d.ts +6 -0
- package/dist/components/checkout/payment-step-form/methods/index.js +21 -0
- package/dist/components/checkout/payment-step-form/methods/index.js.map +1 -0
- package/dist/components/checkout/shipping-step-form.d.ts +3 -0
- package/dist/components/checkout/shipping-step-form.js +53 -0
- package/dist/components/checkout/shipping-step-form.js.map +1 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +9 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/item/product-card.d.ts +7 -0
- package/dist/components/item/product-card.js +9 -0
- package/dist/components/item/product-card.js.map +1 -0
- package/dist/components/item-selector/button.d.ts +4 -0
- package/dist/components/item-selector/button.js +47 -0
- package/dist/components/item-selector/button.js.map +1 -0
- package/dist/components/item-selector/carousel/index.d.ts +12 -0
- package/dist/components/item-selector/carousel/index.js +74 -0
- package/dist/components/item-selector/carousel/index.js.map +1 -0
- package/dist/components/item-selector/carousel/slider.d.ts +8 -0
- package/dist/components/item-selector/carousel/slider.js +12 -0
- package/dist/components/item-selector/carousel/slider.js.map +1 -0
- package/dist/components/item-selector/index.d.ts +2 -0
- package/dist/components/item-selector/index.js +3 -0
- package/dist/components/item-selector/index.js.map +1 -0
- package/dist/components/item-selector/quantity-indicator.d.ts +9 -0
- package/dist/components/item-selector/quantity-indicator.js +16 -0
- package/dist/components/item-selector/quantity-indicator.js.map +1 -0
- package/dist/components/node-tabs/index.d.ts +14 -0
- package/dist/components/node-tabs/index.js +42 -0
- package/dist/components/node-tabs/index.js.map +1 -0
- package/dist/components/node-tabs/node-image.d.ts +6 -0
- package/dist/components/node-tabs/node-image.js +13 -0
- package/dist/components/node-tabs/node-image.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/metering.d.ts +162 -0
- package/dist/metering.js +204 -0
- package/dist/metering.js.map +1 -0
- package/dist/metering.test.d.ts +13 -0
- package/dist/metering.test.js +181 -0
- package/dist/metering.test.js.map +1 -0
- package/dist/service/context.d.ts +8 -0
- package/dist/service/context.js +19 -0
- package/dist/service/context.js.map +1 -0
- package/dist/service/debug.d.ts +10 -0
- package/dist/service/debug.js +30 -0
- package/dist/service/debug.js.map +1 -0
- package/dist/service/impls/standalone/actual-line-item.d.ts +40 -0
- package/dist/service/impls/standalone/actual-line-item.js +84 -0
- package/dist/service/impls/standalone/actual-line-item.js.map +1 -0
- package/dist/service/impls/standalone/get-instance.d.ts +2 -0
- package/dist/service/impls/standalone/get-instance.js +39 -0
- package/dist/service/impls/standalone/get-instance.js.map +1 -0
- package/dist/service/impls/standalone/index.d.ts +67 -0
- package/dist/service/impls/standalone/index.js +416 -0
- package/dist/service/impls/standalone/index.js.map +1 -0
- package/dist/service/impls/standalone/order/firebase.d.ts +6 -0
- package/dist/service/impls/standalone/order/firebase.js +6 -0
- package/dist/service/impls/standalone/order/firebase.js.map +1 -0
- package/dist/service/impls/standalone/order/index.d.ts +30 -0
- package/dist/service/impls/standalone/order/index.js +21 -0
- package/dist/service/impls/standalone/order/index.js.map +1 -0
- package/dist/service/impls/standalone/persistence.d.ts +4 -0
- package/dist/service/impls/standalone/persistence.js +22 -0
- package/dist/service/impls/standalone/persistence.js.map +1 -0
- package/dist/service/path-utils.d.ts +7 -0
- package/dist/service/path-utils.js +16 -0
- package/dist/service/path-utils.js.map +1 -0
- package/dist/service/sep.d.ts +6 -0
- package/dist/service/sep.js +6 -0
- package/dist/service/sep.js.map +1 -0
- package/dist/types/category-node.d.ts +36 -0
- package/dist/types/category-node.js +2 -0
- package/dist/types/category-node.js.map +1 -0
- package/dist/types/checkout.d.ts +33 -0
- package/dist/types/checkout.js +2 -0
- package/dist/types/checkout.js.map +1 -0
- package/dist/types/commerce-config.d.ts +11 -0
- package/dist/types/commerce-config.js +2 -0
- package/dist/types/commerce-config.js.map +1 -0
- package/dist/types/commerce-service.d.ts +109 -0
- package/dist/types/commerce-service.js +2 -0
- package/dist/types/commerce-service.js.map +1 -0
- package/dist/types/family.d.ts +16 -0
- package/dist/types/family.js +2 -0
- package/dist/types/family.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/item-selector.d.ts +72 -0
- package/dist/types/item-selector.js +2 -0
- package/dist/types/item-selector.js.map +1 -0
- package/dist/types/line-item.d.ts +14 -0
- package/dist/types/line-item.js +2 -0
- package/dist/types/line-item.js.map +1 -0
- package/dist/types/multi-family-selector-props.d.ts +16 -0
- package/dist/types/multi-family-selector-props.js +2 -0
- package/dist/types/multi-family-selector-props.js.map +1 -0
- package/dist/types/product.d.ts +15 -0
- package/dist/types/product.js +2 -0
- package/dist/types/product.js.map +1 -0
- package/dist/types/promo.d.ts +7 -0
- package/dist/types/promo.js +2 -0
- package/dist/types/promo.js.map +1 -0
- package/dist/types/selection-ui-specifier.d.ts +40 -0
- package/dist/types/selection-ui-specifier.js +2 -0
- package/dist/types/selection-ui-specifier.js.map +1 -0
- package/dist/types/string-mutator.d.ts +9 -0
- package/dist/types/string-mutator.js +2 -0
- package/dist/types/string-mutator.js.map +1 -0
- package/dist/types/token-separators.d.ts +6 -0
- package/dist/types/token-separators.js +2 -0
- package/dist/types/token-separators.js.map +1 -0
- package/dist/util/analytics.d.ts +9 -0
- package/dist/util/analytics.js +10 -0
- package/dist/util/analytics.js.map +1 -0
- package/dist/util/countries.d.ts +7 -0
- package/dist/util/countries.js +197 -0
- package/dist/util/countries.js.map +1 -0
- package/dist/util/error.d.ts +1 -0
- package/dist/util/error.js +22 -0
- package/dist/util/error.js.map +1 -0
- package/dist/util/index.d.ts +15 -0
- package/dist/util/index.js +54 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/item-selector-options-accessor.d.ts +3 -0
- package/dist/util/item-selector-options-accessor.js +27 -0
- package/dist/util/item-selector-options-accessor.js.map +1 -0
- package/dist/util/line-item-ref.d.ts +8 -0
- package/dist/util/line-item-ref.js +15 -0
- package/dist/util/line-item-ref.js.map +1 -0
- package/dist/util/multi-family-selector-options-accessor.d.ts +3 -0
- package/dist/util/multi-family-selector-options-accessor.js +11 -0
- package/dist/util/multi-family-selector-options-accessor.js.map +1 -0
- package/dist/util/obs-string-mutator.d.ts +8 -0
- package/dist/util/obs-string-mutator.js +15 -0
- package/dist/util/obs-string-mutator.js.map +1 -0
- package/dist/util/product-media-accessor.d.ts +29 -0
- package/dist/util/product-media-accessor.js +22 -0
- package/dist/util/product-media-accessor.js.map +1 -0
- package/dist/util/promo-codes.d.ts +3 -0
- package/dist/util/promo-codes.js +100 -0
- package/dist/util/promo-codes.js.map +1 -0
- package/dist/util/selection-ui-specifiers.d.ts +3 -0
- package/dist/util/selection-ui-specifiers.js +24 -0
- package/dist/util/selection-ui-specifiers.js.map +1 -0
- package/dist/util/square-payment.d.ts +7 -0
- package/dist/util/square-payment.js +37 -0
- package/dist/util/square-payment.js.map +1 -0
- package/dist/util/use-sync-sku-param-w-current-item.d.ts +2 -0
- package/dist/util/use-sync-sku-param-w-current-item.js +61 -0
- package/dist/util/use-sync-sku-param-w-current-item.js.map +1 -0
- package/index.ts +18 -1
- package/metering.test.ts +199 -0
- package/metering.ts +289 -0
- 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.
|
|
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": "
|
|
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.
|
|
37
|
-
"next-usequerystate": "^1.
|
|
31
|
+
"ethers": "^6.16.0",
|
|
32
|
+
"next-usequerystate": "^1.20.0"
|
|
38
33
|
},
|
|
39
34
|
"peerDependencies": {
|
|
40
|
-
"@hanzo/ui": "
|
|
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": "
|
|
55
|
-
"@types/react-dom": "
|
|
56
|
-
"cross-fetch": "^4.
|
|
57
|
-
"typescript": "5.
|
|
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
|
+
}
|