@antseed/node 0.2.152 → 0.2.153

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 (73) hide show
  1. package/dist/buyer-identity-context.d.ts +2 -0
  2. package/dist/buyer-identity-context.d.ts.map +1 -1
  3. package/dist/buyer-identity-context.js +1 -0
  4. package/dist/buyer-identity-context.js.map +1 -1
  5. package/dist/discovery/announcer.d.ts +2 -0
  6. package/dist/discovery/announcer.d.ts.map +1 -1
  7. package/dist/discovery/announcer.js +11 -9
  8. package/dist/discovery/announcer.js.map +1 -1
  9. package/dist/index.d.ts +10 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/metering/index.d.ts +1 -1
  14. package/dist/metering/index.d.ts.map +1 -1
  15. package/dist/metering/index.js.map +1 -1
  16. package/dist/metering/storage.d.ts +66 -6
  17. package/dist/metering/storage.d.ts.map +1 -1
  18. package/dist/metering/storage.js +112 -12
  19. package/dist/metering/storage.js.map +1 -1
  20. package/dist/node.d.ts +30 -0
  21. package/dist/node.d.ts.map +1 -1
  22. package/dist/node.js +93 -2
  23. package/dist/node.js.map +1 -1
  24. package/dist/payments/evm/seller-pools-client.d.ts +9 -0
  25. package/dist/payments/evm/seller-pools-client.d.ts.map +1 -1
  26. package/dist/payments/evm/seller-pools-client.js +23 -0
  27. package/dist/payments/evm/seller-pools-client.js.map +1 -1
  28. package/dist/payments/free-tier-delegation.d.ts +65 -0
  29. package/dist/payments/free-tier-delegation.d.ts.map +1 -0
  30. package/dist/payments/free-tier-delegation.js +145 -0
  31. package/dist/payments/free-tier-delegation.js.map +1 -0
  32. package/dist/payments/free-tier-program.d.ts +97 -0
  33. package/dist/payments/free-tier-program.d.ts.map +1 -0
  34. package/dist/payments/free-tier-program.js +267 -0
  35. package/dist/payments/free-tier-program.js.map +1 -0
  36. package/dist/payments/free-tier-stake-resolver.d.ts +79 -0
  37. package/dist/payments/free-tier-stake-resolver.d.ts.map +1 -0
  38. package/dist/payments/free-tier-stake-resolver.js +152 -0
  39. package/dist/payments/free-tier-stake-resolver.js.map +1 -0
  40. package/dist/payments/free-tier-stake.d.ts +101 -0
  41. package/dist/payments/free-tier-stake.d.ts.map +1 -0
  42. package/dist/payments/free-tier-stake.js +116 -0
  43. package/dist/payments/free-tier-stake.js.map +1 -0
  44. package/dist/payments/index.d.ts +7 -1
  45. package/dist/payments/index.d.ts.map +1 -1
  46. package/dist/payments/index.js +3 -0
  47. package/dist/payments/index.js.map +1 -1
  48. package/dist/payments/seller-free-tier-limiter.d.ts +124 -6
  49. package/dist/payments/seller-free-tier-limiter.d.ts.map +1 -1
  50. package/dist/payments/seller-free-tier-limiter.js +342 -32
  51. package/dist/payments/seller-free-tier-limiter.js.map +1 -1
  52. package/dist/payments/seller-free-usage-manager.d.ts.map +1 -1
  53. package/dist/payments/seller-free-usage-manager.js +5 -1
  54. package/dist/payments/seller-free-usage-manager.js.map +1 -1
  55. package/dist/seller-request-handler.d.ts.map +1 -1
  56. package/dist/seller-request-handler.js +105 -19
  57. package/dist/seller-request-handler.js.map +1 -1
  58. package/dist/storage/migrations/metering/004_add_free_tier_staked_usage.d.ts +3 -0
  59. package/dist/storage/migrations/metering/004_add_free_tier_staked_usage.d.ts.map +1 -0
  60. package/dist/storage/migrations/metering/004_add_free_tier_staked_usage.js +21 -0
  61. package/dist/storage/migrations/metering/004_add_free_tier_staked_usage.js.map +1 -0
  62. package/dist/storage/migrations/metering/005_add_free_tier_scope.d.ts +8 -0
  63. package/dist/storage/migrations/metering/005_add_free_tier_scope.d.ts.map +1 -0
  64. package/dist/storage/migrations/metering/005_add_free_tier_scope.js +23 -0
  65. package/dist/storage/migrations/metering/005_add_free_tier_scope.js.map +1 -0
  66. package/dist/storage/migrations/metering/index.d.ts.map +1 -1
  67. package/dist/storage/migrations/metering/index.js +3 -1
  68. package/dist/storage/migrations/metering/index.js.map +1 -1
  69. package/dist/utils/file-lock.d.ts +12 -0
  70. package/dist/utils/file-lock.d.ts.map +1 -0
  71. package/dist/utils/file-lock.js +58 -0
  72. package/dist/utils/file-lock.js.map +1 -0
  73. package/package.json +11 -3
@@ -1,22 +1,57 @@
1
1
  import { type FreeTierConsumption, type MeteringStorage } from '../metering/storage.js';
2
- export interface SellerFreeTierConfig {
2
+ import { type FreeTierProgram, type FreeTierStakeTier } from './free-tier-program.js';
3
+ import type { FreeTierStakeResolver, FreeTierStakeStanding } from './free-tier-stake-resolver.js';
4
+ /** One free-tier program: base limits and the staked tiers above them. */
5
+ export interface SellerFreeTierProgramConfig {
3
6
  /** Maximum zero-priced requests accepted from one buyer address per window. */
4
7
  maxRequestsPerAddress?: number;
5
8
  /** Maximum zero-priced requests accepted from one remote IP per window. */
6
9
  maxRequestsPerIp?: number;
7
10
  /** Sliding-window duration in milliseconds. Default: 24 hours. */
8
11
  windowMs?: number;
12
+ /**
13
+ * Larger allowances for buyers staked on this seller, by staking power.
14
+ * Default: `DEFAULT_FREE_TIER_STAKE_TIERS`. `[]` turns staked tiers off.
15
+ */
16
+ stakeTiers?: FreeTierStakeTier[];
17
+ /**
18
+ * Serve only buyers whose stake reaches a tier. The base limits are still
19
+ * required: they are the allowance the tier multipliers apply to.
20
+ */
21
+ stakersOnly?: boolean;
22
+ }
23
+ export interface SellerFreeTierConfig extends SellerFreeTierProgramConfig {
24
+ /**
25
+ * Programs for individual services, keyed by announced service id. A listed
26
+ * service uses only its own program and its own counters; the other
27
+ * zero-priced services use the seller-wide program above, or are not limited
28
+ * when there is none. On a priced service the program is a free allowance:
29
+ * requests it covers are served free, and the rest are paid as usual.
30
+ */
31
+ services?: Record<string, SellerFreeTierProgramConfig>;
9
32
  }
10
33
  export interface FreeTierDecision extends FreeTierConsumption {
11
34
  buyerAddress: string;
12
35
  /** Normalized remote IP the decision was keyed on, or null when unknown. */
13
36
  remoteIp: string | null;
37
+ /** Staked standing the decision used, or null for the base limits. */
38
+ standing: FreeTierStakeStanding | null;
39
+ /** Tier the standing earned under this program, or null for the base limits. */
40
+ tier: FreeTierStakeTier | null;
41
+ /** Limit that applied to the binding counter; null when unlimited. */
42
+ limit: number | null;
43
+ /** The program the request was decided under, as advertised. */
44
+ program: FreeTierProgram;
14
45
  }
15
46
  export interface FreeTierConsumeInput {
16
47
  buyerPeerId: string;
17
48
  service: string;
18
49
  /** Raw socket remote address of the buyer connection, if known. */
19
50
  remoteIp?: string | null;
51
+ /** Staked standing from `resolveStanding`; omitted or null applies the base limits. */
52
+ standing?: FreeTierStakeStanding | null;
53
+ /** The service is priced: only its own program applies, never the seller-wide one. */
54
+ paid?: boolean;
20
55
  nowMs?: number;
21
56
  }
22
57
  export declare const DEFAULT_FREE_TIER_WINDOW_MS: number;
@@ -27,17 +62,100 @@ export declare const DEFAULT_FREE_TIER_WINDOW_MS: number;
27
62
  */
28
63
  export declare function normalizeRemoteIp(remoteIp: string | null | undefined): string | null;
29
64
  export declare class SellerFreeTierLimiter {
30
- readonly maxRequestsPerAddress: number | null;
31
- readonly maxRequestsPerIp: number | null;
32
- readonly windowMs: number;
65
+ private readonly _default;
66
+ private readonly _services;
67
+ /** Longest window of any program: usage younger than this is never pruned. */
68
+ private readonly _retentionMs;
33
69
  private readonly _storage;
34
70
  private readonly _inMemoryUsage;
71
+ private _stakeResolver;
72
+ private _chainId;
73
+ /** Last verified delegations per buyer address, in insertion order for eviction. */
74
+ private readonly _delegations;
75
+ /** Priced services, whose allowances never admit a free-usage channel. */
76
+ private _paidServices;
77
+ /** Zero-priced services this seller offers, or null when not told: they decide channel admission. */
78
+ private _freeServices;
35
79
  constructor(config: SellerFreeTierConfig, storage?: MeteringStorage | null);
80
+ /**
81
+ * @deprecated The seller-wide program's per-address limit, or null when it
82
+ * has none (including a limiter with only per-service programs). Use
83
+ * `program(service)` for the limits a service is served under.
84
+ */
85
+ get maxRequestsPerAddress(): number | null;
86
+ /** @deprecated The seller-wide program's per-IP limit, or null. Use `program(service)`. */
87
+ get maxRequestsPerIp(): number | null;
88
+ /** @deprecated The seller-wide program's window, else the default window. Use `program(service)`. */
89
+ get windowMs(): number;
90
+ /**
91
+ * Enable staked tiers. Without a resolver every buyer gets the base limits.
92
+ * `chainId` is the EIP-712 domain delegations are verified against.
93
+ */
94
+ setStakeResolver(resolver: FreeTierStakeResolver | null, chainId?: number): void;
95
+ /** Whether any program has staked tiers, so stake lookups are worth enabling. */
96
+ get hasStakeTiers(): boolean;
97
+ /** Whether staked tiers are live: configured and backed by on-chain reads. */
98
+ get stakeTiersActive(): boolean;
99
+ /**
100
+ * Whether a program applies to `service`: its own, or for a zero-priced
101
+ * service the seller-wide one. A priced service has one only when configured.
102
+ */
103
+ appliesTo(service: string, options?: {
104
+ paid?: boolean;
105
+ }): boolean;
106
+ /**
107
+ * Services that are priced. Their programs are free allowances on paid
108
+ * models, served without a free-usage channel, so channel admission ignores them.
109
+ */
110
+ setPaidServices(services: Iterable<string>): void;
111
+ /** The zero-priced services this seller offers, so channel admission checks exactly their programs. */
112
+ setFreeServices(services: Iterable<string>): void;
113
+ /**
114
+ * The program `service` is served under, or the seller-wide program when
115
+ * `service` is omitted; tiers only when they are enforced. Null when none applies.
116
+ */
117
+ program(service?: string, options?: {
118
+ paid?: boolean;
119
+ }): FreeTierProgram | null;
120
+ /**
121
+ * Peer-metadata capability strings advertising every program: the
122
+ * seller-wide one first, then services in id order. A program that would
123
+ * exceed `budget` strings is skipped whole, never advertised in part.
124
+ */
125
+ capabilities(budget?: number): string[];
126
+ /**
127
+ * Staked standing for this buyer, or null for the base limits. Never throws.
128
+ *
129
+ * `delegationsHeader` is the buyer's `x-antseed-free-tier-delegations`
130
+ * value (from a request or a free usage open), or '' when a request carried
131
+ * none. Verified stakers are remembered for the buyer, so an open from an
132
+ * older buyer, which passes undefined, sees the standing of its requests.
133
+ */
134
+ resolveStanding(buyerPeerId: string, delegationsHeader?: string): Promise<FreeTierStakeStanding | null>;
135
+ private _rememberDelegations;
136
+ /**
137
+ * One line per program: the seller-wide one for free models, then each
138
+ * service's own. A priced service's line is its free allowance before its
139
+ * price applies, as set by `setPaidServices`.
140
+ */
141
+ describePrograms(): string[];
36
142
  describe(): string;
143
+ /** Consume one request for `input.service`. Callers check `appliesTo` first. */
37
144
  consume(input: FreeTierConsumeInput): FreeTierDecision;
38
- /** Check channel-open admission without consuming an inference request. */
39
- check(input: Omit<FreeTierConsumeInput, 'service'>): FreeTierDecision;
145
+ /**
146
+ * Check channel-open admission without consuming an inference request.
147
+ * The channel serves every free service, so it is admitted when the program
148
+ * of some offered free service would still serve the buyer, or when one of
149
+ * them is not limited at all. Otherwise the first refusal (the seller-wide
150
+ * one when it applies) is reported. Without the offered services (see
151
+ * `setFreeServices`), every program of a zero-priced service is considered,
152
+ * and no seller-wide program means some free service is unlimited.
153
+ */
154
+ check(input: Omit<FreeTierConsumeInput, 'service' | 'paid'>): FreeTierDecision | null;
155
+ private _policies;
156
+ private _policyFor;
40
157
  private _evaluate;
158
+ private _bindingLimit;
41
159
  private _evaluateInMemory;
42
160
  private _pruneExpiredBuckets;
43
161
  }
@@ -1 +1 @@
1
- {"version":3,"file":"seller-free-tier-limiter.d.ts","sourceRoot":"","sources":["../../src/payments/seller-free-tier-limiter.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,mBAAmB,EAExB,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,2BAA2B,QAAmB,CAAC;AAG5D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAcpF;AAaD,qBAAa,qBAAqB;IAChC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9C,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+B;gBAElD,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,eAAe,GAAG,IAAW;IAsBhF,QAAQ,IAAI,MAAM;IAOlB,OAAO,CAAC,KAAK,EAAE,oBAAoB,GAAG,gBAAgB;IAItD,2EAA2E;IAC3E,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,GAAG,gBAAgB;IAIrE,OAAO,CAAC,SAAS;IAoBjB,OAAO,CAAC,iBAAiB;IA6BzB,OAAO,CAAC,oBAAoB;CAO7B"}
1
+ {"version":3,"file":"seller-free-tier-limiter.d.ts","sourceRoot":"","sources":["../../src/payments/seller-free-tier-limiter.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,9 @@
1
1
  import { isIP } from 'node:net';
2
2
  import { evaluateFreeTierLimits, } from '../metering/storage.js';
3
3
  import { peerIdToAddress } from '../types/peer.js';
4
+ import { MAX_PEER_CAPABILITY_LENGTH } from '../discovery/metadata-validator.js';
5
+ import { DEFAULT_FREE_TIER_STAKE_TIERS, encodeFreeTierCapabilities, freeTierStakeTierForPower, validateFreeTierStakeTiers, } from './free-tier-program.js';
6
+ import { decodeFreeTierDelegationsHeader, verifyFreeTierDelegations, } from './free-tier-delegation.js';
4
7
  export const DEFAULT_FREE_TIER_WINDOW_MS = 24 * 60 * 60_000;
5
8
  const MAX_IN_MEMORY_KEYS = 20_000;
6
9
  /**
@@ -41,83 +44,390 @@ function expandIpv6(ip) {
41
44
  const zeros = isCompressed ? Array(missing).fill('0') : [];
42
45
  return [...headParts, ...zeros, ...tailParts].map((part) => part.padStart(4, '0'));
43
46
  }
44
- export class SellerFreeTierLimiter {
47
+ /** One validated program and the counter scope it is accounted under. */
48
+ class FreeTierPolicy {
49
+ scope;
45
50
  maxRequestsPerAddress;
46
51
  maxRequestsPerIp;
47
52
  windowMs;
48
- _storage;
49
- _inMemoryUsage = new Map();
50
- constructor(config, storage = null) {
53
+ stakeTiers;
54
+ stakersOnly;
55
+ constructor(scope, config, label) {
56
+ this.scope = scope;
51
57
  const perAddress = config.maxRequestsPerAddress;
52
58
  const perIp = config.maxRequestsPerIp;
53
59
  if (perAddress !== undefined && (!Number.isSafeInteger(perAddress) || perAddress < 1)) {
54
- throw new Error('freeTier.maxRequestsPerAddress must be a positive safe integer');
60
+ throw new Error(`${label}.maxRequestsPerAddress must be a positive safe integer`);
55
61
  }
56
62
  if (perIp !== undefined && (!Number.isSafeInteger(perIp) || perIp < 1)) {
57
- throw new Error('freeTier.maxRequestsPerIp must be a positive safe integer');
63
+ throw new Error(`${label}.maxRequestsPerIp must be a positive safe integer`);
58
64
  }
59
65
  if (perAddress === undefined && perIp === undefined) {
60
- throw new Error('freeTier requires maxRequestsPerAddress and/or maxRequestsPerIp');
66
+ throw new Error(`${label} requires maxRequestsPerAddress and/or maxRequestsPerIp`);
61
67
  }
62
68
  const windowMs = config.windowMs ?? DEFAULT_FREE_TIER_WINDOW_MS;
63
69
  if (!Number.isSafeInteger(windowMs) || windowMs < 1_000) {
64
- throw new Error('freeTier.windowMs must be a safe integer of at least 1000');
70
+ throw new Error(`${label}.windowMs must be a safe integer of at least 1000`);
71
+ }
72
+ const stakeTiers = config.stakeTiers ?? DEFAULT_FREE_TIER_STAKE_TIERS;
73
+ validateFreeTierStakeTiers(stakeTiers, `${label}.stakeTiers`);
74
+ if (config.stakersOnly && stakeTiers.length === 0) {
75
+ throw new Error(`${label}.stakersOnly requires at least one stake tier`);
65
76
  }
66
77
  this.maxRequestsPerAddress = perAddress ?? null;
67
78
  this.maxRequestsPerIp = perIp ?? null;
68
79
  this.windowMs = windowMs;
69
- this._storage = storage;
80
+ this.stakeTiers = Object.freeze([...stakeTiers]);
81
+ this.stakersOnly = config.stakersOnly === true;
82
+ // Peer metadata caps each capability string; a program that cannot be
83
+ // advertised would fail every metadata publish, so refuse it here.
84
+ const serviceId = scope === '' ? undefined : scope;
85
+ const encodings = [
86
+ encodeFreeTierCapabilities(this.program(true), serviceId),
87
+ ...(serviceId !== undefined ? [encodeFreeTierCapabilities(this.program(true), serviceId, { sellerWideTiers: this.stakeTiers })] : []),
88
+ ];
89
+ const tooLong = encodings.flat().find((capability) => capability.length > MAX_PEER_CAPABILITY_LENGTH);
90
+ if (tooLong) {
91
+ throw new Error(`${label} is too large to advertise ("${tooLong}" exceeds ${MAX_PEER_CAPABILITY_LENGTH} characters): lower its limits or window`);
92
+ }
70
93
  }
71
- describe() {
94
+ program(tiersActive) {
95
+ return {
96
+ base: {
97
+ maxRequestsPerAddress: this.maxRequestsPerAddress,
98
+ maxRequestsPerIp: this.maxRequestsPerIp,
99
+ windowMs: this.windowMs,
100
+ ...(this.stakersOnly ? { stakersOnly: true } : {}),
101
+ },
102
+ stakeTiers: tiersActive ? [...this.stakeTiers] : [],
103
+ };
104
+ }
105
+ /** Requests per window a staked tier allows, or null when unlimited. */
106
+ limitForTier(tier) {
107
+ if (tier.multiplier === 'unlimited')
108
+ return null;
109
+ const base = this.maxRequestsPerAddress ?? this.maxRequestsPerIp;
110
+ return Math.min(Number.MAX_SAFE_INTEGER, base * tier.multiplier);
111
+ }
112
+ /**
113
+ * "free requests a day: 100 per buyer address, then paid; staked (a day):
114
+ * 1,000,000 power → 1,000, 10,000,000 power → unlimited". `paid`: the service is priced.
115
+ */
116
+ describe(paid = false) {
117
+ const window = describeWindow(this.windowMs);
72
118
  const parts = [];
73
119
  if (this.maxRequestsPerAddress !== null)
74
- parts.push(`${this.maxRequestsPerAddress} per buyer address`);
120
+ parts.push(`${count(this.maxRequestsPerAddress)} per buyer address`);
75
121
  if (this.maxRequestsPerIp !== null)
76
- parts.push(`${this.maxRequestsPerIp} per IP`);
77
- return `${parts.join(', ')} every ${this.windowMs}ms`;
122
+ parts.push(`${count(this.maxRequestsPerIp)} per IP`);
123
+ const head = this.stakersOnly
124
+ ? `free only for stakers${paid ? ', others pay' : ', others are refused'}`
125
+ : `free requests ${window}: ${parts.join(', ')}${paid ? ', then paid' : ''}`;
126
+ const tiers = this.stakeTiers.map((tier) => {
127
+ const limit = this.limitForTier(tier);
128
+ return `${count(tier.minPower)} power → ${limit === null ? 'unlimited' : count(limit)}`;
129
+ });
130
+ return tiers.length > 0 ? `${head}; staked (${window}): ${tiers.join(', ')}` : head;
131
+ }
132
+ }
133
+ function count(value) {
134
+ return value.toLocaleString('en-US');
135
+ }
136
+ function describeWindow(windowMs) {
137
+ const hours = windowMs / 3_600_000;
138
+ if (hours === 24)
139
+ return 'a day';
140
+ if (hours >= 1 && Number.isInteger(hours))
141
+ return `per ${hours} h`;
142
+ return windowMs % 60_000 === 0 ? `per ${windowMs / 60_000} min` : `per ${windowMs / 1000} s`;
143
+ }
144
+ function hasSellerWideProgram(config) {
145
+ return config.maxRequestsPerAddress !== undefined
146
+ || config.maxRequestsPerIp !== undefined
147
+ || config.windowMs !== undefined
148
+ || config.stakeTiers !== undefined
149
+ || config.stakersOnly !== undefined;
150
+ }
151
+ export class SellerFreeTierLimiter {
152
+ _default;
153
+ _services;
154
+ /** Longest window of any program: usage younger than this is never pruned. */
155
+ _retentionMs;
156
+ _storage;
157
+ _inMemoryUsage = new Map();
158
+ _stakeResolver = null;
159
+ _chainId = 0;
160
+ /** Last verified delegations per buyer address, in insertion order for eviction. */
161
+ _delegations = new Map();
162
+ /** Priced services, whose allowances never admit a free-usage channel. */
163
+ _paidServices = new Set();
164
+ /** Zero-priced services this seller offers, or null when not told: they decide channel admission. */
165
+ _freeServices = null;
166
+ constructor(config, storage = null) {
167
+ const services = Object.entries(config.services ?? {});
168
+ if (!hasSellerWideProgram(config) && services.length === 0) {
169
+ throw new Error('freeTier requires maxRequestsPerAddress and/or maxRequestsPerIp');
170
+ }
171
+ this._default = hasSellerWideProgram(config) ? new FreeTierPolicy('', config, 'freeTier') : null;
172
+ this._services = new Map(services.map(([service, program]) => {
173
+ if (service.length === 0)
174
+ throw new Error('freeTier.services keys must be non-empty service ids');
175
+ return [service, new FreeTierPolicy(service, program, `freeTier.services.${service}`)];
176
+ }));
177
+ this._retentionMs = Math.max(...this._policies().map((policy) => policy.windowMs));
178
+ this._storage = storage;
179
+ }
180
+ /**
181
+ * @deprecated The seller-wide program's per-address limit, or null when it
182
+ * has none (including a limiter with only per-service programs). Use
183
+ * `program(service)` for the limits a service is served under.
184
+ */
185
+ get maxRequestsPerAddress() {
186
+ return this._default?.maxRequestsPerAddress ?? null;
187
+ }
188
+ /** @deprecated The seller-wide program's per-IP limit, or null. Use `program(service)`. */
189
+ get maxRequestsPerIp() {
190
+ return this._default?.maxRequestsPerIp ?? null;
191
+ }
192
+ /** @deprecated The seller-wide program's window, else the default window. Use `program(service)`. */
193
+ get windowMs() {
194
+ return this._default?.windowMs ?? DEFAULT_FREE_TIER_WINDOW_MS;
195
+ }
196
+ /**
197
+ * Enable staked tiers. Without a resolver every buyer gets the base limits.
198
+ * `chainId` is the EIP-712 domain delegations are verified against.
199
+ */
200
+ setStakeResolver(resolver, chainId = 0) {
201
+ this._stakeResolver = resolver;
202
+ this._chainId = chainId;
203
+ }
204
+ /** Whether any program has staked tiers, so stake lookups are worth enabling. */
205
+ get hasStakeTiers() {
206
+ return this._policies().some((policy) => policy.stakeTiers.length > 0);
78
207
  }
208
+ /** Whether staked tiers are live: configured and backed by on-chain reads. */
209
+ get stakeTiersActive() {
210
+ return this._stakeResolver !== null && this.hasStakeTiers;
211
+ }
212
+ /**
213
+ * Whether a program applies to `service`: its own, or for a zero-priced
214
+ * service the seller-wide one. A priced service has one only when configured.
215
+ */
216
+ appliesTo(service, options = {}) {
217
+ return this._policyFor(service, options.paid === true) !== null;
218
+ }
219
+ /**
220
+ * Services that are priced. Their programs are free allowances on paid
221
+ * models, served without a free-usage channel, so channel admission ignores them.
222
+ */
223
+ setPaidServices(services) {
224
+ this._paidServices = new Set(services);
225
+ }
226
+ /** The zero-priced services this seller offers, so channel admission checks exactly their programs. */
227
+ setFreeServices(services) {
228
+ this._freeServices = [...new Set(services)];
229
+ }
230
+ /**
231
+ * The program `service` is served under, or the seller-wide program when
232
+ * `service` is omitted; tiers only when they are enforced. Null when none applies.
233
+ */
234
+ program(service, options = {}) {
235
+ const policy = service === undefined ? this._default : this._policyFor(service, options.paid === true);
236
+ return policy?.program(this.stakeTiersActive) ?? null;
237
+ }
238
+ /**
239
+ * Peer-metadata capability strings advertising every program: the
240
+ * seller-wide one first, then services in id order. A program that would
241
+ * exceed `budget` strings is skipped whole, never advertised in part.
242
+ */
243
+ capabilities(budget = Number.POSITIVE_INFINITY) {
244
+ const active = this.stakeTiersActive;
245
+ const out = [];
246
+ const programs = [
247
+ ...(this._default ? [[undefined, this._default]] : []),
248
+ ...[...this._services].sort(([a], [b]) => a.localeCompare(b)),
249
+ ];
250
+ // Service programs may point at the seller-wide tiers only once those are advertised.
251
+ let sellerWideTiers;
252
+ for (const [service, policy] of programs) {
253
+ const strings = encodeFreeTierCapabilities(policy.program(active), service, sellerWideTiers ? { sellerWideTiers } : {});
254
+ if (out.length + strings.length > budget)
255
+ continue;
256
+ out.push(...strings);
257
+ if (service === undefined)
258
+ sellerWideTiers = policy.program(active).stakeTiers;
259
+ }
260
+ return out;
261
+ }
262
+ /**
263
+ * Staked standing for this buyer, or null for the base limits. Never throws.
264
+ *
265
+ * `delegationsHeader` is the buyer's `x-antseed-free-tier-delegations`
266
+ * value (from a request or a free usage open), or '' when a request carried
267
+ * none. Verified stakers are remembered for the buyer, so an open from an
268
+ * older buyer, which passes undefined, sees the standing of its requests.
269
+ */
270
+ async resolveStanding(buyerPeerId, delegationsHeader) {
271
+ if (!this._stakeResolver || !this.hasStakeTiers)
272
+ return null;
273
+ const buyer = peerIdToAddress(buyerPeerId).toLowerCase();
274
+ if (delegationsHeader !== undefined) {
275
+ const delegations = decodeFreeTierDelegationsHeader(delegationsHeader, buyer);
276
+ this._rememberDelegations(buyer, verifyFreeTierDelegations(delegations, buyer, this._chainId));
277
+ }
278
+ const nowSeconds = Date.now() / 1000;
279
+ const stakers = (this._delegations.get(buyer) ?? [])
280
+ .filter((delegation) => delegation.expiresAt > nowSeconds)
281
+ .map((delegation) => delegation.staker);
282
+ return this._stakeResolver.resolve(buyer, stakers);
283
+ }
284
+ _rememberDelegations(buyer, delegations) {
285
+ this._delegations.delete(buyer);
286
+ if (delegations.length === 0)
287
+ return;
288
+ if (this._delegations.size >= MAX_IN_MEMORY_KEYS) {
289
+ const oldest = this._delegations.keys().next().value;
290
+ if (oldest !== undefined)
291
+ this._delegations.delete(oldest);
292
+ }
293
+ this._delegations.set(buyer, delegations);
294
+ }
295
+ /**
296
+ * One line per program: the seller-wide one for free models, then each
297
+ * service's own. A priced service's line is its free allowance before its
298
+ * price applies, as set by `setPaidServices`.
299
+ */
300
+ describePrograms() {
301
+ const lines = this._default ? [`free models: ${this._default.describe()}`] : [];
302
+ for (const [service, policy] of this._services) {
303
+ lines.push(`${service}: ${policy.describe(this._paidServices.has(service))}`);
304
+ }
305
+ return lines;
306
+ }
307
+ describe() {
308
+ return this.describePrograms().join(' | ');
309
+ }
310
+ /** Consume one request for `input.service`. Callers check `appliesTo` first. */
79
311
  consume(input) {
80
- return this._evaluate(input, input.service);
312
+ const policy = this._policyFor(input.service, input.paid === true);
313
+ if (!policy)
314
+ throw new Error(`no free-tier program applies to ${input.service}`);
315
+ return this._evaluate(policy, input, input.service);
81
316
  }
82
- /** Check channel-open admission without consuming an inference request. */
317
+ /**
318
+ * Check channel-open admission without consuming an inference request.
319
+ * The channel serves every free service, so it is admitted when the program
320
+ * of some offered free service would still serve the buyer, or when one of
321
+ * them is not limited at all. Otherwise the first refusal (the seller-wide
322
+ * one when it applies) is reported. Without the offered services (see
323
+ * `setFreeServices`), every program of a zero-priced service is considered,
324
+ * and no seller-wide program means some free service is unlimited.
325
+ */
83
326
  check(input) {
84
- return this._evaluate(input, null);
327
+ let policies;
328
+ if (this._freeServices) {
329
+ const applicable = this._freeServices.map((service) => this._policyFor(service, false));
330
+ if (applicable.length === 0 || applicable.some((policy) => policy === null))
331
+ return null;
332
+ policies = [...new Set(applicable)]
333
+ .sort((a, b) => (a === this._default ? -1 : b === this._default ? 1 : 0));
334
+ }
335
+ else {
336
+ if (!this._default)
337
+ return null;
338
+ policies = this._policies().filter((policy) => !this._paidServices.has(policy.scope));
339
+ }
340
+ const decisions = policies.map((policy) => this._evaluate(policy, input, null));
341
+ return decisions.find((decision) => decision.allowed) ?? decisions[0];
342
+ }
343
+ _policies() {
344
+ return [...(this._default ? [this._default] : []), ...this._services.values()];
85
345
  }
86
- _evaluate(input, service) {
346
+ _policyFor(service, paid) {
347
+ return this._services.get(service) ?? (paid ? null : this._default);
348
+ }
349
+ _evaluate(policy, input, service) {
87
350
  const nowMs = input.nowMs ?? Date.now();
88
351
  const buyerAddress = peerIdToAddress(input.buyerPeerId).toLowerCase();
89
352
  const remoteIp = normalizeRemoteIp(input.remoteIp);
353
+ const program = policy.program(this.stakeTiersActive);
354
+ // A standing below this program's lowest tier gets the base limits.
355
+ const tier = input.standing ? freeTierStakeTierForPower(policy.stakeTiers, input.standing.power) : null;
356
+ const standing = tier ? input.standing : null;
357
+ if (policy.stakersOnly && !tier) {
358
+ return {
359
+ allowed: false,
360
+ remaining: 0,
361
+ retryAfterMs: 0,
362
+ limitedBy: 'stake_required',
363
+ buyerAddress,
364
+ remoteIp,
365
+ standing: input.standing ?? null,
366
+ tier: null,
367
+ limit: 0,
368
+ program,
369
+ };
370
+ }
371
+ const ownerLimit = tier ? policy.limitForTier(tier) : null;
90
372
  const usage = {
91
373
  buyerAddress,
92
374
  remoteIp,
93
- maxRequestsPerAddress: this.maxRequestsPerAddress,
94
- maxRequestsPerIp: this.maxRequestsPerIp,
95
- windowMs: this.windowMs,
375
+ maxRequestsPerAddress: policy.maxRequestsPerAddress,
376
+ maxRequestsPerIp: policy.maxRequestsPerIp,
377
+ windowMs: policy.windowMs,
378
+ retentionMs: this._retentionMs,
379
+ scope: policy.scope,
96
380
  nowMs,
381
+ stake: standing ? { positionIds: standing.positionIds, limit: ownerLimit } : null,
97
382
  };
98
383
  const decision = this._storage
99
384
  ? (service === null
100
385
  ? this._storage.checkFreeTierRequest(usage)
101
386
  : this._storage.consumeFreeTierRequest({ ...usage, service }))
102
- : this._evaluateInMemory(buyerAddress, remoteIp, nowMs, service !== null);
103
- return { ...decision, buyerAddress, remoteIp };
387
+ : this._evaluateInMemory(policy, buyerAddress, remoteIp, standing, ownerLimit, nowMs, service !== null);
388
+ return {
389
+ ...decision,
390
+ buyerAddress,
391
+ remoteIp,
392
+ standing,
393
+ tier,
394
+ limit: this._bindingLimit(policy, decision, standing, ownerLimit),
395
+ program,
396
+ };
104
397
  }
105
- _evaluateInMemory(buyerAddress, remoteIp, nowMs, consume) {
106
- const windowStart = nowMs - this.windowMs;
398
+ _bindingLimit(policy, decision, standing, ownerLimit) {
399
+ if (standing)
400
+ return ownerLimit;
401
+ if (decision.limitedBy === 'ip')
402
+ return policy.maxRequestsPerIp;
403
+ return policy.maxRequestsPerAddress ?? policy.maxRequestsPerIp;
404
+ }
405
+ _evaluateInMemory(policy, buyerAddress, remoteIp, standing, ownerLimit, nowMs, consume) {
406
+ const windowStart = nowMs - policy.windowMs;
107
407
  const buckets = new Map();
108
408
  const checks = [];
109
409
  const track = (kind, key, limit) => {
110
- const timestamps = (this._inMemoryUsage.get(key) ?? []).filter((timestamp) => timestamp >= windowStart);
111
- buckets.set(key, timestamps);
410
+ const scopedKey = `${policy.scope}|${key}`;
411
+ const timestamps = (this._inMemoryUsage.get(scopedKey) ?? []).filter((timestamp) => timestamp >= windowStart);
412
+ buckets.set(scopedKey, timestamps);
112
413
  checks.push({ kind, limit, count: timestamps.length, oldestTimestamp: timestamps[0] ?? null });
113
414
  };
114
- if (this.maxRequestsPerAddress !== null)
115
- track('address', `address:${buyerAddress}`, this.maxRequestsPerAddress);
116
- if (this.maxRequestsPerIp !== null && remoteIp !== null)
117
- track('ip', `ip:${remoteIp}`, this.maxRequestsPerIp);
118
- const decision = evaluateFreeTierLimits(checks, this.windowMs, nowMs);
415
+ if (standing) {
416
+ // Unlimited stake is not tracked: nothing would ever read the bucket.
417
+ if (ownerLimit !== null) {
418
+ for (const positionId of standing.positionIds)
419
+ track('stake', `position:${positionId}`, ownerLimit);
420
+ }
421
+ }
422
+ else {
423
+ if (policy.maxRequestsPerAddress !== null)
424
+ track('address', `address:${buyerAddress}`, policy.maxRequestsPerAddress);
425
+ if (policy.maxRequestsPerIp !== null && remoteIp !== null)
426
+ track('ip', `ip:${remoteIp}`, policy.maxRequestsPerIp);
427
+ }
428
+ const decision = evaluateFreeTierLimits(checks, policy.windowMs, nowMs);
119
429
  if (decision.allowed && consume) {
120
- this._pruneExpiredBuckets(windowStart);
430
+ this._pruneExpiredBuckets(nowMs - this._retentionMs);
121
431
  const newKeys = [...buckets.keys()].filter((key) => !this._inMemoryUsage.has(key)).length;
122
432
  if (this._inMemoryUsage.size + newKeys > MAX_IN_MEMORY_KEYS) {
123
433
  throw new Error('free-tier limiter capacity reached while persistent metering is unavailable');