hood-alerts 0.1.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 (107) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +369 -0
  3. package/dist/bot/index.cjs +1008 -0
  4. package/dist/bot/index.cjs.map +1 -0
  5. package/dist/bot/index.d.cts +241 -0
  6. package/dist/bot/index.d.ts +241 -0
  7. package/dist/bot/index.js +30 -0
  8. package/dist/bot/index.js.map +1 -0
  9. package/dist/chunk-32GS6XVE.js +732 -0
  10. package/dist/chunk-32GS6XVE.js.map +1 -0
  11. package/dist/chunk-3LHCQA4Z.js +76 -0
  12. package/dist/chunk-3LHCQA4Z.js.map +1 -0
  13. package/dist/chunk-3NHLSKZE.js +1 -0
  14. package/dist/chunk-3NHLSKZE.js.map +1 -0
  15. package/dist/chunk-6JQK5G3Z.js +72 -0
  16. package/dist/chunk-6JQK5G3Z.js.map +1 -0
  17. package/dist/chunk-CBNZ6AZT.js +598 -0
  18. package/dist/chunk-CBNZ6AZT.js.map +1 -0
  19. package/dist/chunk-CIURPPOW.js +1 -0
  20. package/dist/chunk-CIURPPOW.js.map +1 -0
  21. package/dist/chunk-IUOEGBF6.js +49 -0
  22. package/dist/chunk-IUOEGBF6.js.map +1 -0
  23. package/dist/chunk-NBMINJ5E.js +230 -0
  24. package/dist/chunk-NBMINJ5E.js.map +1 -0
  25. package/dist/chunk-O2RY2SWN.js +1 -0
  26. package/dist/chunk-O2RY2SWN.js.map +1 -0
  27. package/dist/chunk-PLMVKTQM.js +1 -0
  28. package/dist/chunk-PLMVKTQM.js.map +1 -0
  29. package/dist/chunk-RSTLW7XH.js +130 -0
  30. package/dist/chunk-RSTLW7XH.js.map +1 -0
  31. package/dist/chunk-SOBZ2FPP.js +355 -0
  32. package/dist/chunk-SOBZ2FPP.js.map +1 -0
  33. package/dist/chunk-VC5MKEY2.js +170 -0
  34. package/dist/chunk-VC5MKEY2.js.map +1 -0
  35. package/dist/chunk-W2GUAWUR.js +147 -0
  36. package/dist/chunk-W2GUAWUR.js.map +1 -0
  37. package/dist/chunk-Y2M3USWZ.js +355 -0
  38. package/dist/chunk-Y2M3USWZ.js.map +1 -0
  39. package/dist/chunk-Z4DJPESQ.js +1216 -0
  40. package/dist/chunk-Z4DJPESQ.js.map +1 -0
  41. package/dist/chunk-ZDM5VNWB.js +1 -0
  42. package/dist/chunk-ZDM5VNWB.js.map +1 -0
  43. package/dist/events/index.cjs +820 -0
  44. package/dist/events/index.cjs.map +1 -0
  45. package/dist/events/index.d.cts +58 -0
  46. package/dist/events/index.d.ts +58 -0
  47. package/dist/events/index.js +69 -0
  48. package/dist/events/index.js.map +1 -0
  49. package/dist/format-6RG4OcyE.d.ts +57 -0
  50. package/dist/format-B7d40f65.d.cts +57 -0
  51. package/dist/index.cjs +4122 -0
  52. package/dist/index.cjs.map +1 -0
  53. package/dist/index.d.cts +21 -0
  54. package/dist/index.d.ts +21 -0
  55. package/dist/index.js +274 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/logger-3phvV_fl.d.cts +185 -0
  58. package/dist/logger-CYza_0eU.d.ts +185 -0
  59. package/dist/logs-D7sYBvSy.d.cts +66 -0
  60. package/dist/logs-D7sYBvSy.d.ts +66 -0
  61. package/dist/notifiers/index.cjs +620 -0
  62. package/dist/notifiers/index.cjs.map +1 -0
  63. package/dist/notifiers/index.d.cts +186 -0
  64. package/dist/notifiers/index.d.ts +186 -0
  65. package/dist/notifiers/index.js +70 -0
  66. package/dist/notifiers/index.js.map +1 -0
  67. package/dist/pricing-BumgLpvB.d.cts +79 -0
  68. package/dist/pricing-BumgLpvB.d.ts +79 -0
  69. package/dist/ratelimit-BKVQG2oY.d.cts +42 -0
  70. package/dist/ratelimit-CGLlXNIS.d.ts +42 -0
  71. package/dist/registry-D3We8Lg6.d.ts +50 -0
  72. package/dist/registry-DePA3IyS.d.cts +50 -0
  73. package/dist/rules/index.cjs +614 -0
  74. package/dist/rules/index.cjs.map +1 -0
  75. package/dist/rules/index.d.cts +166 -0
  76. package/dist/rules/index.d.ts +166 -0
  77. package/dist/rules/index.js +60 -0
  78. package/dist/rules/index.js.map +1 -0
  79. package/dist/schema-DuDMPqpq.d.ts +183 -0
  80. package/dist/schema-NnIdKFl2.d.cts +183 -0
  81. package/dist/service/index.cjs +3774 -0
  82. package/dist/service/index.cjs.map +1 -0
  83. package/dist/service/index.d.cts +262 -0
  84. package/dist/service/index.d.ts +262 -0
  85. package/dist/service/index.js +45 -0
  86. package/dist/service/index.js.map +1 -0
  87. package/dist/service/main.cjs +3759 -0
  88. package/dist/service/main.cjs.map +1 -0
  89. package/dist/service/main.d.cts +1 -0
  90. package/dist/service/main.d.ts +1 -0
  91. package/dist/service/main.js +60 -0
  92. package/dist/service/main.js.map +1 -0
  93. package/dist/sources-CAaQR6-N.d.ts +159 -0
  94. package/dist/sources-CpDOUlzx.d.cts +159 -0
  95. package/dist/telegram-CpodYJrP.d.cts +64 -0
  96. package/dist/telegram-pvphmBwn.d.ts +64 -0
  97. package/dist/tiers/index.cjs +471 -0
  98. package/dist/tiers/index.cjs.map +1 -0
  99. package/dist/tiers/index.d.cts +231 -0
  100. package/dist/tiers/index.d.ts +231 -0
  101. package/dist/tiers/index.js +34 -0
  102. package/dist/tiers/index.js.map +1 -0
  103. package/dist/types-C0ETog04.d.ts +109 -0
  104. package/dist/types-CcoKpStA.d.cts +109 -0
  105. package/dist/types-DFPpTMWo.d.cts +186 -0
  106. package/dist/types-DFPpTMWo.d.ts +186 -0
  107. package/package.json +114 -0
@@ -0,0 +1,231 @@
1
+ import { b as AlertEventKind } from '../types-DFPpTMWo.cjs';
2
+ import { b as Rule } from '../schema-NnIdKFl2.cjs';
3
+ import { Address } from 'viem';
4
+ import { HoodClient } from 'hoodchain';
5
+ import { F as FetchLogRangeOptions } from '../logs-D7sYBvSy.cjs';
6
+ import 'zod';
7
+
8
+ /**
9
+ * Tiering, as an enforced policy layer rather than a marketing page.
10
+ *
11
+ * Every limit here exists because it maps to a real cost the service pays:
12
+ *
13
+ * - `maxSubscriptions` / `maxRulesPerSubscription` bound the per-event fan-out.
14
+ * - `maxRuleComplexity` bounds how much work one rule can ask for.
15
+ * - `allowOnChainLookupFilters` gates the filters that cost an RPC read per
16
+ * candidate event (liquidity, deployer reputation). This is the expensive
17
+ * one, so it is the clearest free/premium line.
18
+ * - `maxAlertsPerHour` bounds delivery throughput.
19
+ * - `deliveryDelayMs` is the latency difference. Free alerts are held briefly;
20
+ * premium alerts go out as soon as the block is confirmed. It is enforced in
21
+ * the outbox (the delivery row carries `not_before`), so it survives a
22
+ * restart instead of being a `setTimeout` that a crash erases.
23
+ * - `allowedKinds` keeps the highest-volume stream (every bonding-curve trade)
24
+ * on the paid tier, because it is a firehose, not an alert.
25
+ */
26
+ /** The two tiers. */
27
+ type Tier = 'free' | 'premium';
28
+ /** What a tier is allowed to do. */
29
+ interface TierPolicy {
30
+ tier: Tier;
31
+ /** Max delivery targets one subscriber may have. */
32
+ maxSubscriptions: number;
33
+ /** Max rules per subscription. */
34
+ maxRulesPerSubscription: number;
35
+ /** Max {@link ruleComplexity} score for a single rule. */
36
+ maxRuleComplexity: number;
37
+ /** Max entries in a rule's token or deployer watchlist. */
38
+ maxWatchlistTokens: number;
39
+ /** Max alerts delivered per subscriber per rolling hour. */
40
+ maxAlertsPerHour: number;
41
+ /** How long alerts are held before delivery, in ms. */
42
+ deliveryDelayMs: number;
43
+ /** May rules use liquidity and reputation filters (each costs an RPC read). */
44
+ allowOnChainLookupFilters: boolean;
45
+ /** Event kinds this tier may subscribe to. */
46
+ allowedKinds: readonly AlertEventKind[];
47
+ }
48
+ /** The shipped policy table. Override per deployment through the service config. */
49
+ declare const TIER_POLICIES: Record<Tier, TierPolicy>;
50
+ /** A policy decision. `allowed: false` always carries a user-facing reason. */
51
+ interface PolicyDecision {
52
+ allowed: boolean;
53
+ /** Present when `allowed` is false. Written to be shown to the subscriber. */
54
+ reason: string | null;
55
+ }
56
+ /** May this subscriber add another subscription? */
57
+ declare function canAddSubscription(policy: TierPolicy, currentCount: number): PolicyDecision;
58
+ /** May this rule be added to a subscription that already has `currentRuleCount` rules? */
59
+ declare function canAddRule(policy: TierPolicy, rule: Rule, currentRuleCount: number): PolicyDecision;
60
+ /** Is this rule within the tier's limits, regardless of how many exist? */
61
+ declare function isRuleAllowed(policy: TierPolicy, rule: Rule): PolicyDecision;
62
+ /** Resolve the policy for a tier. */
63
+ declare function policyFor(tier: Tier, overrides?: Partial<Record<Tier, Partial<TierPolicy>>>): TierPolicy;
64
+
65
+ /**
66
+ * Entitlements: who is premium, and why.
67
+ *
68
+ * The service resolves a subscriber's tier through exactly one interface, so
69
+ * there is a single place to audit and a single place to swap the commercial
70
+ * model. Two implementations ship:
71
+ *
72
+ * 1. {@link createStaticEntitlementProvider} is the documented default. Premium
73
+ * subscribers come from configuration (`PREMIUM_SUBSCRIBERS` in the
74
+ * environment). It has no dependencies, works offline, and is what a
75
+ * self-hoster running the bot for their own community wants.
76
+ * 2. {@link createUsdgEntitlementProvider} is a working on-chain rail. It reads
77
+ * USDG `Transfer` logs to the operator's receiving address from a wallet
78
+ * the subscriber has linked, and accrues subscription time from those real
79
+ * payments. It needs no facilitator, no card processor and no third-party
80
+ * service: USDG is the chain's own dollar and the ledger is the chain.
81
+ *
82
+ * Wallet linking is signature-verified (EIP-191 `personal_sign` over a
83
+ * server-issued nonce), so a subscriber cannot claim someone else's payments
84
+ * by pasting their address.
85
+ */
86
+ /** A resolved entitlement. */
87
+ interface Entitlement {
88
+ subscriberId: string;
89
+ tier: Tier;
90
+ /** When premium lapses, ms since epoch. `null` for free or for a permanent grant. */
91
+ expiresAtMs: number | null;
92
+ /** Which provider decided this, for support and for `/tier`. */
93
+ source: string;
94
+ }
95
+ /** The single entitlement chokepoint. */
96
+ interface EntitlementProvider {
97
+ /** Resolve a subscriber's tier. Must never throw: fall back to free. */
98
+ get(subscriberId: string): Promise<Entitlement>;
99
+ }
100
+ /**
101
+ * Configuration-driven entitlements: the documented default.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const entitlements = createStaticEntitlementProvider({
106
+ * premiumSubscribers: ['telegram:12345', 'discord:987654321'],
107
+ * })
108
+ * ```
109
+ */
110
+ declare function createStaticEntitlementProvider(options: {
111
+ premiumSubscribers?: readonly string[];
112
+ /** Treat everyone as premium. For a private self-hosted deployment. */
113
+ allPremium?: boolean;
114
+ }): EntitlementProvider;
115
+ /** Lookup of the wallet a subscriber has verifiably linked. */
116
+ interface WalletLinkStore {
117
+ /** The linked, signature-verified wallet for a subscriber, or `null`. */
118
+ walletOf(subscriberId: string): Promise<Address | null>;
119
+ }
120
+ /** Options for {@link createUsdgEntitlementProvider}. */
121
+ interface UsdgEntitlementOptions {
122
+ client: HoodClient;
123
+ /** The operator's receiving address. Payments to any other address are ignored. */
124
+ receiver: Address;
125
+ /** Price of one subscription period, in whole USDG. */
126
+ pricePerPeriodUsdg: number;
127
+ /** Length of a period in days. @defaultValue `30` */
128
+ periodDays?: number;
129
+ /** Where verified wallet links come from. */
130
+ links: WalletLinkStore;
131
+ /** First block to scan for payments. @defaultValue `0n` */
132
+ fromBlock?: bigint;
133
+ /** Cache lifetime per subscriber, in ms. @defaultValue `300_000` (5 minutes) */
134
+ ttlMs?: number;
135
+ /** Retry/bisect options for the payment scan. */
136
+ logOptions?: FetchLogRangeOptions;
137
+ /** Clock injection point for tests. @defaultValue `Date.now` */
138
+ now?: () => number;
139
+ }
140
+ /**
141
+ * Accrue an expiry from a payment history. Each payment buys
142
+ * `floor(amount / price)` periods, starting from whenever the current
143
+ * entitlement would have lapsed, so renewing early extends rather than
144
+ * overwrites.
145
+ *
146
+ * Exported because it is the part worth testing on its own: everything else in
147
+ * the provider is a log query.
148
+ */
149
+ declare function accrueExpiry(payments: readonly {
150
+ atMs: number;
151
+ amountUsdg: number;
152
+ }[], pricePerPeriodUsdg: number, periodDays: number): number | null;
153
+ /**
154
+ * On-chain USDG entitlements.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const entitlements = createUsdgEntitlementProvider({
159
+ * client: hood,
160
+ * receiver: '0xYourReceivingAddress',
161
+ * pricePerPeriodUsdg: 25,
162
+ * links: store, // the service's SQLite store implements WalletLinkStore
163
+ * })
164
+ * const entitlement = await entitlements.get('telegram:12345')
165
+ * ```
166
+ */
167
+ declare function createUsdgEntitlementProvider(options: UsdgEntitlementOptions): EntitlementProvider;
168
+ /**
169
+ * Chain several providers: the first one that returns premium wins. Lets an
170
+ * operator grant comps by configuration while still selling on chain.
171
+ */
172
+ declare function chainEntitlementProviders(...providers: readonly EntitlementProvider[]): EntitlementProvider;
173
+ /** The message a subscriber signs to link a wallet. */
174
+ declare function linkMessage(subscriberId: string, nonce: string): string;
175
+ /**
176
+ * Verify a wallet link signature (EIP-191 `personal_sign`).
177
+ *
178
+ * The subscriber signs {@link linkMessage} with the wallet that pays, and the
179
+ * service checks the signature before storing the link. Without this step,
180
+ * anyone could paste a paying subscriber's address and inherit their
181
+ * entitlement.
182
+ *
183
+ * @returns the checksummed address on success, `null` on any failure (bad
184
+ * signature shape, wrong signer, malformed address).
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * const nonce = crypto.randomUUID()
189
+ * // the user signs linkMessage('telegram:12345', nonce) in their wallet
190
+ * const address = await verifyWalletLink('telegram:12345', nonce, wallet, signature)
191
+ * if (address) await store.linkWallet('telegram:12345', address)
192
+ * ```
193
+ */
194
+ declare function verifyWalletLink(subscriberId: string, nonce: string, address: string, signature: string): Promise<Address | null>;
195
+
196
+ /**
197
+ * The entitlement chokepoint.
198
+ *
199
+ * Every tier question in the codebase goes through this object: the bot asks
200
+ * it before accepting a subscription or a rule, and the dispatcher asks it
201
+ * before queuing a delivery. Nothing else reads {@link TIER_POLICIES}
202
+ * directly. One place to audit, one place to change the commercial model, no
203
+ * chance of the bot and the dispatcher disagreeing about who is premium.
204
+ */
205
+ interface EntitlementGate {
206
+ /** Resolve tier, policy and expiry for a subscriber. */
207
+ resolve(subscriberId: string): Promise<{
208
+ entitlement: Entitlement;
209
+ policy: TierPolicy;
210
+ }>;
211
+ /** May they add another subscription? */
212
+ checkAddSubscription(subscriberId: string, currentCount: number): Promise<PolicyDecision>;
213
+ /** May they add this rule to a subscription with `currentRuleCount` rules? */
214
+ checkAddRule(subscriberId: string, rule: Rule, currentRuleCount: number): Promise<PolicyDecision>;
215
+ /** Is this rule within their tier, ignoring counts (used when editing)? */
216
+ checkRule(subscriberId: string, rule: Rule): Promise<PolicyDecision>;
217
+ /** How long a delivery for this subscriber is held before sending, in ms. */
218
+ deliveryDelayMs(subscriberId: string): Promise<number>;
219
+ /** Have they exhausted their hourly delivery budget? */
220
+ checkThroughput(subscriberId: string, deliveredInLastHour: number): Promise<PolicyDecision>;
221
+ }
222
+ /** Options for {@link createEntitlementGate}. */
223
+ interface EntitlementGateOptions {
224
+ provider: EntitlementProvider;
225
+ /** Per-deployment overrides of the shipped tier table. */
226
+ policyOverrides?: Partial<Record<Tier, Partial<TierPolicy>>>;
227
+ }
228
+ /** Build the gate. */
229
+ declare function createEntitlementGate(options: EntitlementGateOptions): EntitlementGate;
230
+
231
+ export { type Entitlement, type EntitlementGate, type EntitlementGateOptions, type EntitlementProvider, type PolicyDecision, TIER_POLICIES, type Tier, type TierPolicy, type UsdgEntitlementOptions, type WalletLinkStore, accrueExpiry, canAddRule, canAddSubscription, chainEntitlementProviders, createEntitlementGate, createStaticEntitlementProvider, createUsdgEntitlementProvider, isRuleAllowed, linkMessage, policyFor, verifyWalletLink };
@@ -0,0 +1,231 @@
1
+ import { b as AlertEventKind } from '../types-DFPpTMWo.js';
2
+ import { b as Rule } from '../schema-DuDMPqpq.js';
3
+ import { Address } from 'viem';
4
+ import { HoodClient } from 'hoodchain';
5
+ import { F as FetchLogRangeOptions } from '../logs-D7sYBvSy.js';
6
+ import 'zod';
7
+
8
+ /**
9
+ * Tiering, as an enforced policy layer rather than a marketing page.
10
+ *
11
+ * Every limit here exists because it maps to a real cost the service pays:
12
+ *
13
+ * - `maxSubscriptions` / `maxRulesPerSubscription` bound the per-event fan-out.
14
+ * - `maxRuleComplexity` bounds how much work one rule can ask for.
15
+ * - `allowOnChainLookupFilters` gates the filters that cost an RPC read per
16
+ * candidate event (liquidity, deployer reputation). This is the expensive
17
+ * one, so it is the clearest free/premium line.
18
+ * - `maxAlertsPerHour` bounds delivery throughput.
19
+ * - `deliveryDelayMs` is the latency difference. Free alerts are held briefly;
20
+ * premium alerts go out as soon as the block is confirmed. It is enforced in
21
+ * the outbox (the delivery row carries `not_before`), so it survives a
22
+ * restart instead of being a `setTimeout` that a crash erases.
23
+ * - `allowedKinds` keeps the highest-volume stream (every bonding-curve trade)
24
+ * on the paid tier, because it is a firehose, not an alert.
25
+ */
26
+ /** The two tiers. */
27
+ type Tier = 'free' | 'premium';
28
+ /** What a tier is allowed to do. */
29
+ interface TierPolicy {
30
+ tier: Tier;
31
+ /** Max delivery targets one subscriber may have. */
32
+ maxSubscriptions: number;
33
+ /** Max rules per subscription. */
34
+ maxRulesPerSubscription: number;
35
+ /** Max {@link ruleComplexity} score for a single rule. */
36
+ maxRuleComplexity: number;
37
+ /** Max entries in a rule's token or deployer watchlist. */
38
+ maxWatchlistTokens: number;
39
+ /** Max alerts delivered per subscriber per rolling hour. */
40
+ maxAlertsPerHour: number;
41
+ /** How long alerts are held before delivery, in ms. */
42
+ deliveryDelayMs: number;
43
+ /** May rules use liquidity and reputation filters (each costs an RPC read). */
44
+ allowOnChainLookupFilters: boolean;
45
+ /** Event kinds this tier may subscribe to. */
46
+ allowedKinds: readonly AlertEventKind[];
47
+ }
48
+ /** The shipped policy table. Override per deployment through the service config. */
49
+ declare const TIER_POLICIES: Record<Tier, TierPolicy>;
50
+ /** A policy decision. `allowed: false` always carries a user-facing reason. */
51
+ interface PolicyDecision {
52
+ allowed: boolean;
53
+ /** Present when `allowed` is false. Written to be shown to the subscriber. */
54
+ reason: string | null;
55
+ }
56
+ /** May this subscriber add another subscription? */
57
+ declare function canAddSubscription(policy: TierPolicy, currentCount: number): PolicyDecision;
58
+ /** May this rule be added to a subscription that already has `currentRuleCount` rules? */
59
+ declare function canAddRule(policy: TierPolicy, rule: Rule, currentRuleCount: number): PolicyDecision;
60
+ /** Is this rule within the tier's limits, regardless of how many exist? */
61
+ declare function isRuleAllowed(policy: TierPolicy, rule: Rule): PolicyDecision;
62
+ /** Resolve the policy for a tier. */
63
+ declare function policyFor(tier: Tier, overrides?: Partial<Record<Tier, Partial<TierPolicy>>>): TierPolicy;
64
+
65
+ /**
66
+ * Entitlements: who is premium, and why.
67
+ *
68
+ * The service resolves a subscriber's tier through exactly one interface, so
69
+ * there is a single place to audit and a single place to swap the commercial
70
+ * model. Two implementations ship:
71
+ *
72
+ * 1. {@link createStaticEntitlementProvider} is the documented default. Premium
73
+ * subscribers come from configuration (`PREMIUM_SUBSCRIBERS` in the
74
+ * environment). It has no dependencies, works offline, and is what a
75
+ * self-hoster running the bot for their own community wants.
76
+ * 2. {@link createUsdgEntitlementProvider} is a working on-chain rail. It reads
77
+ * USDG `Transfer` logs to the operator's receiving address from a wallet
78
+ * the subscriber has linked, and accrues subscription time from those real
79
+ * payments. It needs no facilitator, no card processor and no third-party
80
+ * service: USDG is the chain's own dollar and the ledger is the chain.
81
+ *
82
+ * Wallet linking is signature-verified (EIP-191 `personal_sign` over a
83
+ * server-issued nonce), so a subscriber cannot claim someone else's payments
84
+ * by pasting their address.
85
+ */
86
+ /** A resolved entitlement. */
87
+ interface Entitlement {
88
+ subscriberId: string;
89
+ tier: Tier;
90
+ /** When premium lapses, ms since epoch. `null` for free or for a permanent grant. */
91
+ expiresAtMs: number | null;
92
+ /** Which provider decided this, for support and for `/tier`. */
93
+ source: string;
94
+ }
95
+ /** The single entitlement chokepoint. */
96
+ interface EntitlementProvider {
97
+ /** Resolve a subscriber's tier. Must never throw: fall back to free. */
98
+ get(subscriberId: string): Promise<Entitlement>;
99
+ }
100
+ /**
101
+ * Configuration-driven entitlements: the documented default.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const entitlements = createStaticEntitlementProvider({
106
+ * premiumSubscribers: ['telegram:12345', 'discord:987654321'],
107
+ * })
108
+ * ```
109
+ */
110
+ declare function createStaticEntitlementProvider(options: {
111
+ premiumSubscribers?: readonly string[];
112
+ /** Treat everyone as premium. For a private self-hosted deployment. */
113
+ allPremium?: boolean;
114
+ }): EntitlementProvider;
115
+ /** Lookup of the wallet a subscriber has verifiably linked. */
116
+ interface WalletLinkStore {
117
+ /** The linked, signature-verified wallet for a subscriber, or `null`. */
118
+ walletOf(subscriberId: string): Promise<Address | null>;
119
+ }
120
+ /** Options for {@link createUsdgEntitlementProvider}. */
121
+ interface UsdgEntitlementOptions {
122
+ client: HoodClient;
123
+ /** The operator's receiving address. Payments to any other address are ignored. */
124
+ receiver: Address;
125
+ /** Price of one subscription period, in whole USDG. */
126
+ pricePerPeriodUsdg: number;
127
+ /** Length of a period in days. @defaultValue `30` */
128
+ periodDays?: number;
129
+ /** Where verified wallet links come from. */
130
+ links: WalletLinkStore;
131
+ /** First block to scan for payments. @defaultValue `0n` */
132
+ fromBlock?: bigint;
133
+ /** Cache lifetime per subscriber, in ms. @defaultValue `300_000` (5 minutes) */
134
+ ttlMs?: number;
135
+ /** Retry/bisect options for the payment scan. */
136
+ logOptions?: FetchLogRangeOptions;
137
+ /** Clock injection point for tests. @defaultValue `Date.now` */
138
+ now?: () => number;
139
+ }
140
+ /**
141
+ * Accrue an expiry from a payment history. Each payment buys
142
+ * `floor(amount / price)` periods, starting from whenever the current
143
+ * entitlement would have lapsed, so renewing early extends rather than
144
+ * overwrites.
145
+ *
146
+ * Exported because it is the part worth testing on its own: everything else in
147
+ * the provider is a log query.
148
+ */
149
+ declare function accrueExpiry(payments: readonly {
150
+ atMs: number;
151
+ amountUsdg: number;
152
+ }[], pricePerPeriodUsdg: number, periodDays: number): number | null;
153
+ /**
154
+ * On-chain USDG entitlements.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const entitlements = createUsdgEntitlementProvider({
159
+ * client: hood,
160
+ * receiver: '0xYourReceivingAddress',
161
+ * pricePerPeriodUsdg: 25,
162
+ * links: store, // the service's SQLite store implements WalletLinkStore
163
+ * })
164
+ * const entitlement = await entitlements.get('telegram:12345')
165
+ * ```
166
+ */
167
+ declare function createUsdgEntitlementProvider(options: UsdgEntitlementOptions): EntitlementProvider;
168
+ /**
169
+ * Chain several providers: the first one that returns premium wins. Lets an
170
+ * operator grant comps by configuration while still selling on chain.
171
+ */
172
+ declare function chainEntitlementProviders(...providers: readonly EntitlementProvider[]): EntitlementProvider;
173
+ /** The message a subscriber signs to link a wallet. */
174
+ declare function linkMessage(subscriberId: string, nonce: string): string;
175
+ /**
176
+ * Verify a wallet link signature (EIP-191 `personal_sign`).
177
+ *
178
+ * The subscriber signs {@link linkMessage} with the wallet that pays, and the
179
+ * service checks the signature before storing the link. Without this step,
180
+ * anyone could paste a paying subscriber's address and inherit their
181
+ * entitlement.
182
+ *
183
+ * @returns the checksummed address on success, `null` on any failure (bad
184
+ * signature shape, wrong signer, malformed address).
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * const nonce = crypto.randomUUID()
189
+ * // the user signs linkMessage('telegram:12345', nonce) in their wallet
190
+ * const address = await verifyWalletLink('telegram:12345', nonce, wallet, signature)
191
+ * if (address) await store.linkWallet('telegram:12345', address)
192
+ * ```
193
+ */
194
+ declare function verifyWalletLink(subscriberId: string, nonce: string, address: string, signature: string): Promise<Address | null>;
195
+
196
+ /**
197
+ * The entitlement chokepoint.
198
+ *
199
+ * Every tier question in the codebase goes through this object: the bot asks
200
+ * it before accepting a subscription or a rule, and the dispatcher asks it
201
+ * before queuing a delivery. Nothing else reads {@link TIER_POLICIES}
202
+ * directly. One place to audit, one place to change the commercial model, no
203
+ * chance of the bot and the dispatcher disagreeing about who is premium.
204
+ */
205
+ interface EntitlementGate {
206
+ /** Resolve tier, policy and expiry for a subscriber. */
207
+ resolve(subscriberId: string): Promise<{
208
+ entitlement: Entitlement;
209
+ policy: TierPolicy;
210
+ }>;
211
+ /** May they add another subscription? */
212
+ checkAddSubscription(subscriberId: string, currentCount: number): Promise<PolicyDecision>;
213
+ /** May they add this rule to a subscription with `currentRuleCount` rules? */
214
+ checkAddRule(subscriberId: string, rule: Rule, currentRuleCount: number): Promise<PolicyDecision>;
215
+ /** Is this rule within their tier, ignoring counts (used when editing)? */
216
+ checkRule(subscriberId: string, rule: Rule): Promise<PolicyDecision>;
217
+ /** How long a delivery for this subscriber is held before sending, in ms. */
218
+ deliveryDelayMs(subscriberId: string): Promise<number>;
219
+ /** Have they exhausted their hourly delivery budget? */
220
+ checkThroughput(subscriberId: string, deliveredInLastHour: number): Promise<PolicyDecision>;
221
+ }
222
+ /** Options for {@link createEntitlementGate}. */
223
+ interface EntitlementGateOptions {
224
+ provider: EntitlementProvider;
225
+ /** Per-deployment overrides of the shipped tier table. */
226
+ policyOverrides?: Partial<Record<Tier, Partial<TierPolicy>>>;
227
+ }
228
+ /** Build the gate. */
229
+ declare function createEntitlementGate(options: EntitlementGateOptions): EntitlementGate;
230
+
231
+ export { type Entitlement, type EntitlementGate, type EntitlementGateOptions, type EntitlementProvider, type PolicyDecision, TIER_POLICIES, type Tier, type TierPolicy, type UsdgEntitlementOptions, type WalletLinkStore, accrueExpiry, canAddRule, canAddSubscription, chainEntitlementProviders, createEntitlementGate, createStaticEntitlementProvider, createUsdgEntitlementProvider, isRuleAllowed, linkMessage, policyFor, verifyWalletLink };
@@ -0,0 +1,34 @@
1
+ import "../chunk-O2RY2SWN.js";
2
+ import {
3
+ TIER_POLICIES,
4
+ canAddRule,
5
+ canAddSubscription,
6
+ createEntitlementGate,
7
+ isRuleAllowed,
8
+ policyFor
9
+ } from "../chunk-RSTLW7XH.js";
10
+ import {
11
+ accrueExpiry,
12
+ chainEntitlementProviders,
13
+ createStaticEntitlementProvider,
14
+ createUsdgEntitlementProvider,
15
+ linkMessage,
16
+ verifyWalletLink
17
+ } from "../chunk-W2GUAWUR.js";
18
+ import "../chunk-VC5MKEY2.js";
19
+ import "../chunk-3LHCQA4Z.js";
20
+ export {
21
+ TIER_POLICIES,
22
+ accrueExpiry,
23
+ canAddRule,
24
+ canAddSubscription,
25
+ chainEntitlementProviders,
26
+ createEntitlementGate,
27
+ createStaticEntitlementProvider,
28
+ createUsdgEntitlementProvider,
29
+ isRuleAllowed,
30
+ linkMessage,
31
+ policyFor,
32
+ verifyWalletLink
33
+ };
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,109 @@
1
+ import { P as Platform } from './schema-DuDMPqpq.js';
2
+
3
+ /** A Discord embed field. */
4
+ interface DiscordEmbedField {
5
+ name: string;
6
+ value: string;
7
+ inline?: boolean;
8
+ }
9
+ /** The subset of the Discord embed object this package produces. */
10
+ interface DiscordEmbed {
11
+ title: string;
12
+ url?: string;
13
+ description?: string;
14
+ /** Decimal RGB, per Discord's API. */
15
+ color: number;
16
+ fields: DiscordEmbedField[];
17
+ footer?: {
18
+ text: string;
19
+ };
20
+ /** ISO-8601 timestamp. Omitted when the block time was not read. */
21
+ timestamp?: string;
22
+ }
23
+ /**
24
+ * One alert, rendered for every platform up front.
25
+ *
26
+ * Rendering once and letting each notifier pick its representation keeps the
27
+ * escaping rules next to each other (where they can be compared and tested
28
+ * together) and means a fan-out to Telegram and Discord formats the event
29
+ * once, not twice.
30
+ */
31
+ interface RenderedAlert {
32
+ /** Telegram body, escaped for {@link RenderedAlert.parseMode}. */
33
+ text: string;
34
+ parseMode: 'HTML' | 'MarkdownV2';
35
+ /** Discord embed representation of the same alert. */
36
+ embed: DiscordEmbed;
37
+ /** Unstyled one-line summary, for logs and for platforms added later. */
38
+ plain: string;
39
+ }
40
+ /** What happened when a notifier tried to deliver. */
41
+ interface DeliveryResult {
42
+ ok: boolean;
43
+ /** HTTP status of the final attempt, or `null` if the request never completed. */
44
+ status: number | null;
45
+ /** How many requests were made, including the successful one. */
46
+ attempts: number;
47
+ /** Platform message id, when the API returned one. */
48
+ messageId: string | null;
49
+ /** Failure detail. `null` on success. */
50
+ error: string | null;
51
+ /**
52
+ * `true` when the failure is worth retrying later (5xx, network, exhausted
53
+ * rate limit). `false` for a permanent rejection such as a bad chat id, so
54
+ * the service can disable a dead subscription instead of retrying forever.
55
+ */
56
+ retryable: boolean;
57
+ }
58
+ /** The one interface every delivery adapter implements. */
59
+ interface Notifier {
60
+ readonly platform: Platform;
61
+ /** Adapter flavour, for logs and metrics: `bot`, `webhook`. */
62
+ readonly kind: string;
63
+ /**
64
+ * Deliver a rendered alert to a platform-specific target: a Telegram chat
65
+ * id, a Discord channel id, or a Discord webhook URL.
66
+ */
67
+ send(target: string, alert: RenderedAlert): Promise<DeliveryResult>;
68
+ }
69
+ /** The `fetch` shape the adapters use, so tests can substitute a double. */
70
+ type FetchLike = (input: string, init?: {
71
+ method?: string;
72
+ headers?: Record<string, string>;
73
+ body?: string;
74
+ }) => Promise<FetchLikeResponse>;
75
+ /** The response shape the adapters read. */
76
+ interface FetchLikeResponse {
77
+ ok: boolean;
78
+ status: number;
79
+ headers: {
80
+ get(name: string): string | null;
81
+ };
82
+ text(): Promise<string>;
83
+ }
84
+ /** Options common to every HTTP-backed notifier. */
85
+ interface NotifierHttpOptions {
86
+ /** Injected `fetch`. Defaults to the global. */
87
+ fetch?: FetchLike;
88
+ /** Max attempts per delivery, including the first. @defaultValue `4` */
89
+ maxAttempts?: number;
90
+ /** Base backoff in ms for retryable failures, doubled per attempt. @defaultValue `500` */
91
+ backoffMs?: number;
92
+ /**
93
+ * Upper bound on how long a single delivery will wait for a rate limit, in
94
+ * ms. A `retry_after` beyond this gives up and reports a retryable failure
95
+ * rather than blocking the whole dispatch loop.
96
+ * @defaultValue `60_000`
97
+ */
98
+ maxWaitMs?: number;
99
+ /** Sleep injection point for tests. */
100
+ sleep?: (ms: number) => Promise<void>;
101
+ /** Request timeout in ms. @defaultValue `15_000` */
102
+ timeoutMs?: number;
103
+ }
104
+ /** A successful send result. */
105
+ declare function delivered(status: number, attempts: number, messageId: string | null): DeliveryResult;
106
+ /** A failed send result. */
107
+ declare function failed(status: number | null, attempts: number, error: string, retryable: boolean): DeliveryResult;
108
+
109
+ export { type DeliveryResult as D, type FetchLike as F, type Notifier as N, type RenderedAlert as R, type DiscordEmbed as a, type DiscordEmbedField as b, type FetchLikeResponse as c, type NotifierHttpOptions as d, delivered as e, failed as f };