@usagekit/core 0.1.0 → 0.4.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.
- package/dist/contracts.d.ts +166 -9
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -3
- package/dist/money.d.ts +2 -0
- package/dist/money.js +2 -0
- package/dist/policy.d.ts +11 -1
- package/dist/policy.js +29 -0
- package/dist/providers.d.ts +256 -0
- package/dist/providers.js +54 -0
- package/dist/quantity.d.ts +2 -0
- package/dist/quantity.js +6 -0
- package/dist/wire.js +6 -0
- package/package.json +1 -1
package/dist/contracts.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ export type Scope = {
|
|
|
37
37
|
accessCredential?: AccessCredential;
|
|
38
38
|
/** Which provider secret version was used. Rotation identity, not caller identity. */
|
|
39
39
|
providerCredentialVersion?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Connection labels snapshotted at reservation: 1..16 unique entries, each 1..64 characters
|
|
42
|
+
* matching [a-z0-9][a-z0-9_.:-]*, sorted before storage. Attribution only: excluded from
|
|
43
|
+
* reserve identity, so a replay keeps the first reservation's tags.
|
|
44
|
+
*/
|
|
45
|
+
tags?: readonly string[];
|
|
40
46
|
};
|
|
41
47
|
export type FundingSource = "byok" | "platform";
|
|
42
48
|
export type Surface = "app" | "programmatic";
|
|
@@ -62,6 +68,7 @@ export type BudgetWindow = {
|
|
|
62
68
|
* Each variant names one constraint in a namespace.
|
|
63
69
|
* A platform_pool bounds spend shared by many principals and applies to operations
|
|
64
70
|
* naming that pool in ReserveInput. It is not the platform principal's own budget.
|
|
71
|
+
* A tag bounds every operation whose scope.tags contains it, across principals.
|
|
65
72
|
* A wallet is not a budget: the host ledger owns balances, holds and charges,
|
|
66
73
|
* coordinated through the host credit bridge. A wallet-level monthly cap may still
|
|
67
74
|
* be a principal Budget because the cap is a constraint, not a balance.
|
|
@@ -86,16 +93,49 @@ export type BudgetScope = {
|
|
|
86
93
|
kind: "platform_pool";
|
|
87
94
|
namespace: string;
|
|
88
95
|
poolId: string;
|
|
96
|
+
} | {
|
|
97
|
+
kind: "tag";
|
|
98
|
+
namespace: string;
|
|
99
|
+
tag: string;
|
|
89
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* surface selects the traffic a budget bounds: "any" matches every operation, a Surface
|
|
103
|
+
* matches operations whose source belongs to that surface group (see sourcesOf), and a
|
|
104
|
+
* Source matches exactly that source. Hosts set surface and source at authentication.
|
|
105
|
+
*/
|
|
90
106
|
export type Budget = {
|
|
91
107
|
id: string;
|
|
92
108
|
version: number;
|
|
93
109
|
scope: BudgetScope;
|
|
94
|
-
surface: Surface | "any";
|
|
110
|
+
surface: Surface | Source | "any";
|
|
95
111
|
unit: string;
|
|
96
112
|
limit: Quantity | null;
|
|
97
113
|
window: BudgetWindow;
|
|
98
|
-
|
|
114
|
+
/** block denies past limit. allow admits with a warning; wire and stored rows may still say the deprecated alias for allow. */
|
|
115
|
+
onExceed: "block" | "allow";
|
|
116
|
+
/** Only with onExceed allow: same unit as limit and greater than it. Exceeding it denies admission. */
|
|
117
|
+
hardLimit?: Quantity;
|
|
118
|
+
/** At most eight thresholds in ascending order; each crossing is reported once per epoch. */
|
|
119
|
+
alerts?: readonly BudgetAlert[];
|
|
120
|
+
};
|
|
121
|
+
/** percent is an integer 1..100 of limit; a Quantity threshold is in the budget unit. */
|
|
122
|
+
export type BudgetAlert = {
|
|
123
|
+
at: {
|
|
124
|
+
percent: number;
|
|
125
|
+
} | Quantity;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Reported by the command whose admitted or settled figures first reach the threshold.
|
|
129
|
+
* The store records each crossing atomically per budget id, epoch and threshold, so a
|
|
130
|
+
* replay returns the stored crossings and no later command reports the same one again.
|
|
131
|
+
*/
|
|
132
|
+
export type BudgetAlertCrossed = {
|
|
133
|
+
budgetId: string;
|
|
134
|
+
budgetVersion: number;
|
|
135
|
+
epoch: string;
|
|
136
|
+
at: BudgetAlert["at"];
|
|
137
|
+
used: Quantity;
|
|
138
|
+
reserved: Quantity;
|
|
99
139
|
};
|
|
100
140
|
export type LifecycleState = "reserved" | "dispatch_intended" | "pending" | "settled" | "released";
|
|
101
141
|
export type Certainty = "measured" | "estimated" | "unknown";
|
|
@@ -138,14 +178,14 @@ export type Receipt = {
|
|
|
138
178
|
* Store selects all applying budgets atomically inside reserve; Meter's pre-resolution
|
|
139
179
|
* is advisory and never sufficient for admission.
|
|
140
180
|
* Match namespace and the bound: principal, group, connection, accessCredential by
|
|
141
|
-
* kind and id, or poolId listed in platformPools. Match surface or
|
|
181
|
+
* kind and id, or poolId listed in platformPools. Match "any", the surface or the source.
|
|
142
182
|
* Each applicable budget's unit must appear in estimate. A missing bounded unit is
|
|
143
183
|
* a Meter validation error, not permission to bypass the bound. Store re-checks this
|
|
144
184
|
* against current budgets. Units without budgets are recorded without a bound.
|
|
145
185
|
* Use the current budget version at reservation time; snapshot its resolved epoch
|
|
146
186
|
* and version into budgetEpochs. Check every applying budget in one atomic command.
|
|
147
|
-
* Return the first exceeded bound in deterministic host-policy order; default
|
|
148
|
-
* platform_pool, principal, group, connection, access_credential.
|
|
187
|
+
* Return the first exceeded bound in deterministic host-policy order; the default
|
|
188
|
+
* is defaultBudgetOrder: platform_pool, principal, group, tag, connection, access_credential.
|
|
149
189
|
* A guest session is actor, not budget owner. The platform or a dedicated guest
|
|
150
190
|
* principal owns the operation; its pool and host-enforced session limit both apply.
|
|
151
191
|
*/
|
|
@@ -168,6 +208,10 @@ export type ReserveInput = {
|
|
|
168
208
|
provider: string;
|
|
169
209
|
operation: string;
|
|
170
210
|
estimate: readonly Quantity[];
|
|
211
|
+
/** Set by a Meter that resolved the estimate from a catalog; hosts may set it themselves. */
|
|
212
|
+
estimateSource?: EstimateSource;
|
|
213
|
+
/** "<provider>:<validFrom>" of the list price row behind the estimate. */
|
|
214
|
+
providerPriceVersion?: string;
|
|
171
215
|
correlationId?: string;
|
|
172
216
|
parentOperationId?: string;
|
|
173
217
|
};
|
|
@@ -205,11 +249,13 @@ export type ReserveResult = ValidationFailure | {
|
|
|
205
249
|
replayed: false;
|
|
206
250
|
operation: Operation;
|
|
207
251
|
warnings: readonly AllowanceExceeded[];
|
|
252
|
+
alerts: readonly BudgetAlertCrossed[];
|
|
208
253
|
} | {
|
|
209
254
|
outcome: "reserved";
|
|
210
255
|
replayed: true;
|
|
211
256
|
operation: Operation;
|
|
212
257
|
warnings: readonly AllowanceExceeded[];
|
|
258
|
+
alerts: readonly BudgetAlertCrossed[];
|
|
213
259
|
} | {
|
|
214
260
|
outcome: "exceeded";
|
|
215
261
|
exceeded: AllowanceExceeded;
|
|
@@ -218,6 +264,84 @@ export type ReserveResult = ValidationFailure | {
|
|
|
218
264
|
outcome: "conflict";
|
|
219
265
|
reason: "semantic_mismatch";
|
|
220
266
|
operation: Operation;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The connection's tracking policy is passthrough for this operation, or the catalog marks it
|
|
270
|
+
* free: counted with state passthrough, never reserved. The caller may forward the call.
|
|
271
|
+
*/
|
|
272
|
+
| {
|
|
273
|
+
outcome: "passthrough";
|
|
274
|
+
replayed: boolean;
|
|
275
|
+
operation: null;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* No estimate was given and the catalog does not know the operation: counted with state
|
|
279
|
+
* unpriced, never reserved. Host policy decides whether to forward the call.
|
|
280
|
+
*/
|
|
281
|
+
| {
|
|
282
|
+
outcome: "unpriced";
|
|
283
|
+
replayed: boolean;
|
|
284
|
+
operation: null;
|
|
285
|
+
};
|
|
286
|
+
/** Where a reserve estimate came from, in resolution order. */
|
|
287
|
+
export type EstimateSource = "manual" | "measured" | "list" | "unknown";
|
|
288
|
+
/**
|
|
289
|
+
* Meter reserve input. With a catalog, estimate may be omitted: the Meter resolves it from the
|
|
290
|
+
* catalog for the connection and the request options. Store always receives a ReserveInput.
|
|
291
|
+
*/
|
|
292
|
+
export type MeterReserveInput = Omit<ReserveInput, "estimate"> & {
|
|
293
|
+
/** Omitted: the Meter resolves it through its catalog and the connection policy. */
|
|
294
|
+
estimate?: readonly Quantity[];
|
|
295
|
+
/** Request options that move the price, read from the request by the descriptor. */
|
|
296
|
+
options?: Readonly<Record<string, string>>;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* Non-operation outcomes, counted per state and never reserved. Metered requests are the
|
|
300
|
+
* operations themselves and are not counted here.
|
|
301
|
+
*/
|
|
302
|
+
export type RequestState = "passthrough" | "unpriced" | "cached" | "rate_limited";
|
|
303
|
+
/**
|
|
304
|
+
* One counted request. commandId is the replay key: an identical replay counts once and returns
|
|
305
|
+
* replayed: true; a changed payload under the same commandId is a conflict. Counts carry no
|
|
306
|
+
* content and never touch budgets.
|
|
307
|
+
*/
|
|
308
|
+
export type CountRequestInput = {
|
|
309
|
+
commandId: string;
|
|
310
|
+
scope: Scope;
|
|
311
|
+
surface: Surface;
|
|
312
|
+
source: Source;
|
|
313
|
+
provider: string;
|
|
314
|
+
operation: string;
|
|
315
|
+
state: RequestState;
|
|
316
|
+
platformPools?: readonly string[];
|
|
317
|
+
};
|
|
318
|
+
export type CountRequestResult = ValidationFailure | {
|
|
319
|
+
outcome: "counted";
|
|
320
|
+
replayed: boolean;
|
|
321
|
+
} | {
|
|
322
|
+
outcome: "conflict";
|
|
323
|
+
reason: "command_mismatch";
|
|
324
|
+
};
|
|
325
|
+
/** Requests counted at store time in [from, to), with the scope and access rules of usage. */
|
|
326
|
+
export type RequestCountsQuery = {
|
|
327
|
+
scope: UsageScope;
|
|
328
|
+
from: string;
|
|
329
|
+
to: string;
|
|
330
|
+
connection?: string;
|
|
331
|
+
groupBy: readonly ("provider" | "operation" | "connection" | "source" | "day")[];
|
|
332
|
+
/** Rows per page, 1..1000, default 1000; truncated reports more rows. */
|
|
333
|
+
limit?: number;
|
|
334
|
+
};
|
|
335
|
+
/** One row per dimension combination and state, ordered by dimensions then state. */
|
|
336
|
+
export type RequestCountRow = {
|
|
337
|
+
dimensions: Readonly<Record<string, string>>;
|
|
338
|
+
state: RequestState;
|
|
339
|
+
count: bigint;
|
|
340
|
+
};
|
|
341
|
+
export type RequestCountsPage = {
|
|
342
|
+
rows: readonly RequestCountRow[];
|
|
343
|
+
asOf: string;
|
|
344
|
+
truncated: boolean;
|
|
221
345
|
};
|
|
222
346
|
export type OperationRef = Pick<Scope, "namespace" | "principal"> & {
|
|
223
347
|
operationId: string;
|
|
@@ -341,6 +465,7 @@ export type SettleResult = ValidationFailure | {
|
|
|
341
465
|
outcome: "settled";
|
|
342
466
|
replayed: boolean;
|
|
343
467
|
operation: Operation;
|
|
468
|
+
alerts: readonly BudgetAlertCrossed[];
|
|
344
469
|
} | {
|
|
345
470
|
outcome: "rejected";
|
|
346
471
|
reason: "not_holder" | "lease_expired" | "version_conflict" | "invalid_state" | "receipt_conflict";
|
|
@@ -390,14 +515,18 @@ export type AccessContext = {
|
|
|
390
515
|
canReadBillingDetail: boolean;
|
|
391
516
|
canManageBudgets: boolean;
|
|
392
517
|
};
|
|
393
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* groupBy: "principal" is allowed only for group or namespace scope.
|
|
520
|
+
* "platform_pool" and "tag" explode one row per pool or tag of an operation, with an
|
|
521
|
+
* empty dimension when the operation has none; those rows are not additive across tags.
|
|
522
|
+
*/
|
|
394
523
|
export type UsageQuery = {
|
|
395
524
|
scope: UsageScope;
|
|
396
525
|
from: string;
|
|
397
526
|
to: string;
|
|
398
527
|
connection?: string;
|
|
399
528
|
units: readonly string[];
|
|
400
|
-
groupBy: readonly ("principal" | "provider" | "operation" | "surface" | "source" | "connection" | "day" | "access_credential" | "platform_pool")[];
|
|
529
|
+
groupBy: readonly ("principal" | "provider" | "operation" | "surface" | "source" | "connection" | "day" | "access_credential" | "platform_pool" | "funding_source" | "tag")[];
|
|
401
530
|
cursor?: string;
|
|
402
531
|
limit?: number;
|
|
403
532
|
};
|
|
@@ -415,6 +544,27 @@ export type UsagePage = {
|
|
|
415
544
|
watermark: string;
|
|
416
545
|
nextCursor?: string;
|
|
417
546
|
};
|
|
547
|
+
/**
|
|
548
|
+
* Operations whose createdAt lies in [from, to), in one lifecycle state of states (all when
|
|
549
|
+
* omitted), ordered by createdAt then operationId. Same scope and access rules as usage.
|
|
550
|
+
* Pagination mirrors usage: the first page fixes membership and order at its watermark; later
|
|
551
|
+
* pages take the cursor with the unchanged query and return each operation as currently stored.
|
|
552
|
+
*/
|
|
553
|
+
export type OperationsQuery = {
|
|
554
|
+
scope: UsageScope;
|
|
555
|
+
from: string;
|
|
556
|
+
to: string;
|
|
557
|
+
states?: readonly LifecycleState[];
|
|
558
|
+
connection?: string;
|
|
559
|
+
cursor?: string;
|
|
560
|
+
limit?: number;
|
|
561
|
+
};
|
|
562
|
+
export type OperationsPage = {
|
|
563
|
+
operations: readonly Operation[];
|
|
564
|
+
asOf: string;
|
|
565
|
+
watermark: string;
|
|
566
|
+
nextCursor?: string;
|
|
567
|
+
};
|
|
418
568
|
/** Budgets defined for exactly this scope, regardless of any operation. */
|
|
419
569
|
export type DefinedBudgetsQuery = {
|
|
420
570
|
scope: BudgetScope;
|
|
@@ -428,6 +578,8 @@ export type DefinedBudgetsQuery = {
|
|
|
428
578
|
export type ApplicableBudgetsQuery = {
|
|
429
579
|
scope: Scope;
|
|
430
580
|
surface: Surface;
|
|
581
|
+
/** Include budgets bound to exactly this source. Omitted lists surface and any budgets only. */
|
|
582
|
+
source?: Source;
|
|
431
583
|
units: readonly string[];
|
|
432
584
|
platformPools?: readonly string[];
|
|
433
585
|
};
|
|
@@ -451,10 +603,12 @@ export type ReadResult<T> = ValidationFailure | {
|
|
|
451
603
|
} | {
|
|
452
604
|
outcome: "forbidden";
|
|
453
605
|
};
|
|
454
|
-
/** Data-only budget denial. A P1 implementation may wrap it in a runtime error class. */
|
|
606
|
+
/** Data-only budget denial or warning. A P1 implementation may wrap it in a runtime error class. */
|
|
455
607
|
export interface AllowanceExceeded {
|
|
456
608
|
readonly code: "allowance_exceeded";
|
|
457
609
|
readonly budget: Budget;
|
|
610
|
+
/** Which bound was crossed: limit warns on allow budgets and denies on block budgets; hardLimit always denies. */
|
|
611
|
+
readonly boundary: "limit" | "hardLimit";
|
|
458
612
|
readonly used: Quantity;
|
|
459
613
|
readonly reserved: Quantity;
|
|
460
614
|
readonly resetsAt: string | null;
|
|
@@ -478,7 +632,8 @@ export type ExpireReservationsResult = ValidationFailure | {
|
|
|
478
632
|
*/
|
|
479
633
|
export interface Meter {
|
|
480
634
|
expireReservations(input: ExpireReservationsInput): Promise<ExpireReservationsResult>;
|
|
481
|
-
reserve(input:
|
|
635
|
+
reserve(input: MeterReserveInput): Promise<ReserveResult>;
|
|
636
|
+
countRequest(input: CountRequestInput): Promise<CountRequestResult>;
|
|
482
637
|
markDispatchIntent(input: DispatchIntentInput): Promise<DispatchGrant>;
|
|
483
638
|
renewLease(input: LeaseRenewalInput): Promise<LeaseRenewal>;
|
|
484
639
|
claimForRecovery(input: RecoveryClaimInput): Promise<RecoveryClaim>;
|
|
@@ -487,6 +642,8 @@ export interface Meter {
|
|
|
487
642
|
releaseUndispatched(input: ReleaseInput): Promise<ReleaseResult>;
|
|
488
643
|
getOperation(access: AccessContext, input: OperationRef): Promise<ReadResult<Operation | null>>;
|
|
489
644
|
usage(access: AccessContext, query: UsageQuery): Promise<ReadResult<UsagePage>>;
|
|
645
|
+
listOperations(access: AccessContext, query: OperationsQuery): Promise<ReadResult<OperationsPage>>;
|
|
646
|
+
requestCounts(access: AccessContext, query: RequestCountsQuery): Promise<ReadResult<RequestCountsPage>>;
|
|
490
647
|
definedBudgets(access: AccessContext, query: DefinedBudgetsQuery): Promise<ReadResult<readonly Budget[]>>;
|
|
491
648
|
applicableBudgets(access: AccessContext, query: ApplicableBudgetsQuery): Promise<ReadResult<readonly BudgetStatus[]>>;
|
|
492
649
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
export type { Money, Quantity, AccessCredential, Scope, FundingSource, Surface, Source, BudgetWindow, BudgetScope, Budget, LifecycleState, Certainty, Measurement, Cost, Receipt, ReserveInput, ReserveResult, Operation, OperationRef, OperationCommand, Lease, DispatchGrant, DispatchIntentInput, LeaseRenewalInput, LeaseRenewal, RecoveryClaimInput, RecoveryClaim, Authority, SettleInput, ReleaseInput, CorrectionInput, SettleResult, ReleaseResult, UsageScope, AccessContext, UsageQuery, UsageRow, UsagePage, DefinedBudgetsQuery, ApplicableBudgetsQuery, BudgetStatus, ReadResult, AllowanceExceeded, Meter, } from "./contracts.js";
|
|
1
|
+
export type { Money, Quantity, AccessCredential, Scope, FundingSource, Surface, Source, BudgetWindow, BudgetScope, Budget, BudgetAlert, BudgetAlertCrossed, LifecycleState, Certainty, Measurement, Cost, Receipt, ReserveInput, ReserveResult, Operation, OperationRef, OperationCommand, Lease, DispatchGrant, DispatchIntentInput, LeaseRenewalInput, LeaseRenewal, RecoveryClaimInput, RecoveryClaim, Authority, SettleInput, ReleaseInput, CorrectionInput, SettleResult, ReleaseResult, UsageScope, AccessContext, UsageQuery, UsageRow, UsagePage, OperationsQuery, OperationsPage, DefinedBudgetsQuery, ApplicableBudgetsQuery, BudgetStatus, ReadResult, AllowanceExceeded, Meter, MeterReserveInput, EstimateSource, RequestState, CountRequestInput, CountRequestResult, RequestCountsQuery, RequestCountRow, RequestCountsPage, } from "./contracts.js";
|
|
2
|
+
export { requestStates } from "./policy.js";
|
|
2
3
|
export { resolveWindow } from "./windows.js";
|
|
3
4
|
export type { ResolvedWindow } from "./windows.js";
|
|
4
|
-
export { fromDecimalString, toDecimalString, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
|
|
5
|
-
export { normalizeScale, add as addQuantity, compare as compareQuantity } from "./quantity.js";
|
|
5
|
+
export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
|
|
6
|
+
export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
|
|
6
7
|
export type { ValidationFailure, AdmissionPolicy, BudgetOwner } from "./contracts.js";
|
|
7
|
-
export { defaultBudgetOrder } from "./policy.js";
|
|
8
|
+
export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
|
|
8
9
|
export type { ExpireReservationsInput, ExpireReservationsResult } from "./contracts.js";
|
|
9
10
|
export { decodeMeterJson } from "./wire.js";
|
|
11
|
+
export type { ProviderId, OperationId, ProviderAuth, BillingUnit, ProviderPlan, PriceRow, RequestMatch, ProviderOperation, BalanceProbe, PriceListSource, BillingExport, ProviderDescriptor, BalanceSnapshot, BillingLine, DescriptorProblem, PricingCatalog, ConnectionPolicy, TrackingPolicy, MeasuredPrice, } from "./providers.js";
|
|
12
|
+
export { validateDescriptor } from "./providers.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { requestStates } from "./policy.js";
|
|
1
2
|
export { resolveWindow } from "./windows.js";
|
|
2
|
-
export { fromDecimalString, toDecimalString, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
|
|
3
|
-
export { normalizeScale, add as addQuantity, compare as compareQuantity } from "./quantity.js";
|
|
4
|
-
export { defaultBudgetOrder } from "./policy.js";
|
|
3
|
+
export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
|
|
4
|
+
export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
|
|
5
|
+
export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
|
|
5
6
|
export { decodeMeterJson } from "./wire.js";
|
|
7
|
+
export { validateDescriptor } from "./providers.js";
|
package/dist/money.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export declare function toDecimalString(m: Money): string;
|
|
|
6
6
|
export declare const add: (a: Money, b: Money) => Money;
|
|
7
7
|
export declare const sub: (a: Money, b: Money) => Money;
|
|
8
8
|
export declare const compare: (a: Money, b: Money) => -1 | 0 | 1;
|
|
9
|
+
/** Exact cents text with four fractional digits; a named alias of toDecimalString for views. */
|
|
10
|
+
export declare const formatMoney: (m: Money) => string;
|
package/dist/money.js
CHANGED
|
@@ -15,3 +15,5 @@ export function toDecimalString(m) {
|
|
|
15
15
|
export const add = (a, b) => ({ currency: "USD", units: a.units + b.units });
|
|
16
16
|
export const sub = (a, b) => ({ currency: "USD", units: a.units - b.units });
|
|
17
17
|
export const compare = (a, b) => a.units < b.units ? -1 : a.units > b.units ? 1 : 0;
|
|
18
|
+
/** Exact cents text with four fractional digits; a named alias of toDecimalString for views. */
|
|
19
|
+
export const formatMoney = toDecimalString;
|
package/dist/policy.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
import type { BudgetScope } from "./contracts.js";
|
|
1
|
+
import type { BudgetScope, RequestState, Source, Surface } from "./contracts.js";
|
|
2
|
+
/** Single source of the default admission order; hosts may permute it, never shorten it. */
|
|
2
3
|
export declare const defaultBudgetOrder: readonly BudgetScope["kind"][];
|
|
4
|
+
export declare const tagPattern: RegExp;
|
|
5
|
+
/** Sorted unique tags, or null when the list breaks the 1..16 count, format or uniqueness rules. */
|
|
6
|
+
export declare function normalizeTags(tags: readonly string[]): string[] | null;
|
|
7
|
+
/** Sources a surface budget covers. Every Source belongs to exactly one Surface. */
|
|
8
|
+
export declare const sourcesOf: (surface: Surface) => readonly Source[];
|
|
9
|
+
/** True when a budget surface value bounds an operation with this surface and source. */
|
|
10
|
+
export declare const surfaceMatches: (budgetSurface: Surface | Source | "any", surface: Surface, source?: Source) => boolean;
|
|
11
|
+
/** Every counted request state, in display order. */
|
|
12
|
+
export declare const requestStates: readonly RequestState[];
|
package/dist/policy.js
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
|
+
/** Single source of the default admission order; hosts may permute it, never shorten it. */
|
|
1
2
|
export const defaultBudgetOrder = Object.freeze([
|
|
2
3
|
"platform_pool",
|
|
3
4
|
"principal",
|
|
4
5
|
"group",
|
|
6
|
+
"tag",
|
|
5
7
|
"connection",
|
|
6
8
|
"access_credential",
|
|
7
9
|
]);
|
|
10
|
+
export const tagPattern = /^[a-z0-9][a-z0-9_.:-]{0,63}$/;
|
|
11
|
+
/** Sorted unique tags, or null when the list breaks the 1..16 count, format or uniqueness rules. */
|
|
12
|
+
export function normalizeTags(tags) {
|
|
13
|
+
if (!Array.isArray(tags) || tags.length < 1 || tags.length > 16)
|
|
14
|
+
return null;
|
|
15
|
+
if (tags.some((t) => typeof t !== "string" || !tagPattern.test(t)))
|
|
16
|
+
return null;
|
|
17
|
+
const sorted = [...tags].sort();
|
|
18
|
+
return sorted.some((t, n) => n > 0 && sorted[n - 1] === t) ? null : sorted;
|
|
19
|
+
}
|
|
20
|
+
const surfaceSources = Object.freeze({
|
|
21
|
+
app: Object.freeze(["app", "worker"]),
|
|
22
|
+
programmatic: Object.freeze(["api", "sdk", "cli", "mcp", "proxy"]),
|
|
23
|
+
});
|
|
24
|
+
/** Sources a surface budget covers. Every Source belongs to exactly one Surface. */
|
|
25
|
+
export const sourcesOf = (surface) => surfaceSources[surface];
|
|
26
|
+
/** True when a budget surface value bounds an operation with this surface and source. */
|
|
27
|
+
export const surfaceMatches = (budgetSurface, surface, source) => budgetSurface === "any" ||
|
|
28
|
+
budgetSurface === surface ||
|
|
29
|
+
(source !== undefined && budgetSurface === source);
|
|
30
|
+
/** Every counted request state, in display order. */
|
|
31
|
+
export const requestStates = Object.freeze([
|
|
32
|
+
"passthrough",
|
|
33
|
+
"unpriced",
|
|
34
|
+
"cached",
|
|
35
|
+
"rate_limited",
|
|
36
|
+
]);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { EstimateSource, Money, Quantity } from "./contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Provider descriptors: data that tells usagekit how to price, route, meter and reconcile one
|
|
4
|
+
* provider. Descriptors live in `@usagekit/providers`; the core contract does not depend on
|
|
5
|
+
* them. See docs/PROVIDERS.md.
|
|
6
|
+
*/
|
|
7
|
+
export type ProviderId = string;
|
|
8
|
+
/**
|
|
9
|
+
* Operation identity is the pricing key. Manual overrides, tracking policy and budgets per
|
|
10
|
+
* operation all key on OperationId, never on a raw path.
|
|
11
|
+
*/
|
|
12
|
+
export type OperationId = string;
|
|
13
|
+
export type ProviderAuth = {
|
|
14
|
+
kind: "query";
|
|
15
|
+
param: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "header";
|
|
18
|
+
name: string;
|
|
19
|
+
prefix?: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "basic";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "bearer";
|
|
24
|
+
};
|
|
25
|
+
export type BillingUnit = "cents" | "units" | "requests";
|
|
26
|
+
/**
|
|
27
|
+
* allowanceMode mirrors how the provider behaves past the allowance: prepaid balances and
|
|
28
|
+
* no-overage plans are hard, plans that bill excess are soft.
|
|
29
|
+
*/
|
|
30
|
+
export type ProviderPlan = {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
cycle: "monthly_plan" | "prepaid" | "trial" | "none";
|
|
34
|
+
allowance?: {
|
|
35
|
+
unit: BillingUnit;
|
|
36
|
+
value: bigint;
|
|
37
|
+
};
|
|
38
|
+
allowanceMode: "hard" | "soft";
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* One list price. The descriptor never holds negotiated prices, discounts or volume tiers beyond
|
|
42
|
+
* the public list; those are manual prices on the connection.
|
|
43
|
+
*/
|
|
44
|
+
export type PriceRow = {
|
|
45
|
+
/** ISO date. */
|
|
46
|
+
validFrom: string;
|
|
47
|
+
operation: OperationId;
|
|
48
|
+
/** Plan id; absent means any plan. */
|
|
49
|
+
plan?: string;
|
|
50
|
+
/** Request options that select this row. Options are request data, never guessed. */
|
|
51
|
+
option?: Readonly<Record<string, string>>;
|
|
52
|
+
/** Applies past the plan allowance. */
|
|
53
|
+
overage?: true;
|
|
54
|
+
unit: BillingUnit;
|
|
55
|
+
/** Exact nonnegative decimal in that unit. */
|
|
56
|
+
perUnit: string;
|
|
57
|
+
/** URL of the public price source. */
|
|
58
|
+
source: string;
|
|
59
|
+
/** ISO date of the last check against the source. */
|
|
60
|
+
checkedAt: string;
|
|
61
|
+
};
|
|
62
|
+
export type RequestMatch = {
|
|
63
|
+
method: "GET" | "POST" | "PUT" | "DELETE" | "ANY";
|
|
64
|
+
/** Path pattern with :params (one segment) and a trailing * (any rest). */
|
|
65
|
+
path: string;
|
|
66
|
+
query?: Readonly<Record<string, string | RegExp>>;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Free endpoints are operations too, with billable false. They are matched, counted and never
|
|
70
|
+
* reserved. A descriptor may advertise an operation only when at least one fixture exists for it.
|
|
71
|
+
*/
|
|
72
|
+
export type ProviderOperation = {
|
|
73
|
+
id: OperationId;
|
|
74
|
+
label: string;
|
|
75
|
+
billable: boolean;
|
|
76
|
+
match: readonly RequestMatch[];
|
|
77
|
+
/** Options that move the price; values are extracted from the request by `options`. */
|
|
78
|
+
optionKeys?: readonly string[];
|
|
79
|
+
/**
|
|
80
|
+
* Cost is known from the response body, only after a later call, or never per call.
|
|
81
|
+
* response: settle on stream close. deferred: settle with unknown cost, keep pending, resolve
|
|
82
|
+
* through the task-get operation or billing export. none: settle with the estimate as
|
|
83
|
+
* estimated, reconcile from balance deltas or export.
|
|
84
|
+
*/
|
|
85
|
+
costEvidence: "response" | "deferred" | "none";
|
|
86
|
+
/** Idempotent replay is safe for this operation when the provider returns the same result. */
|
|
87
|
+
cacheable?: {
|
|
88
|
+
keyParams: readonly string[];
|
|
89
|
+
defaultTtlMs: number;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/** Account identity is returned to the host as data. Duplicate detection is a host decision. */
|
|
93
|
+
export type BalanceProbe = {
|
|
94
|
+
/** The free account endpoint. */
|
|
95
|
+
operation: OperationId;
|
|
96
|
+
fields: {
|
|
97
|
+
/** JSON pointer. */
|
|
98
|
+
remaining?: string;
|
|
99
|
+
remainingUnit?: BillingUnit;
|
|
100
|
+
allowance?: string;
|
|
101
|
+
used?: string;
|
|
102
|
+
resetsAt?: string;
|
|
103
|
+
plan?: string;
|
|
104
|
+
/** E-mail or login, for duplicate detection. */
|
|
105
|
+
accountIdentity?: string;
|
|
106
|
+
rateLimitPerHour?: string;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* A price list from an endpoint is a refresh source for prices, not a replacement: a pull
|
|
111
|
+
* request updates the rows with the fetched values and the check date; runtime never fetches
|
|
112
|
+
* prices to decide admission.
|
|
113
|
+
*/
|
|
114
|
+
export type PriceListSource = {
|
|
115
|
+
kind: "static";
|
|
116
|
+
} | {
|
|
117
|
+
kind: "endpoint";
|
|
118
|
+
operation: OperationId;
|
|
119
|
+
pointer: string;
|
|
120
|
+
mapping: "provider-specific";
|
|
121
|
+
};
|
|
122
|
+
export type BillingExport = {
|
|
123
|
+
kind: "task-history" | "daily-csv" | "none";
|
|
124
|
+
/** For API-backed history. */
|
|
125
|
+
operation?: OperationId;
|
|
126
|
+
granularity: "per-request" | "per-day";
|
|
127
|
+
matchKey: "providerRequestId" | "window";
|
|
128
|
+
};
|
|
129
|
+
export type ProviderDescriptor = {
|
|
130
|
+
id: ProviderId;
|
|
131
|
+
label: string;
|
|
132
|
+
/** Origin and optional base path, for example "https://api.example.com". */
|
|
133
|
+
upstream: string;
|
|
134
|
+
auth: ProviderAuth;
|
|
135
|
+
stripRequestHeaders?: readonly string[];
|
|
136
|
+
billing: {
|
|
137
|
+
unit: BillingUnit;
|
|
138
|
+
cycle: ProviderPlan["cycle"];
|
|
139
|
+
};
|
|
140
|
+
plans: readonly ProviderPlan[];
|
|
141
|
+
prices: readonly PriceRow[];
|
|
142
|
+
priceList: PriceListSource;
|
|
143
|
+
operations: readonly ProviderOperation[];
|
|
144
|
+
balance?: BalanceProbe;
|
|
145
|
+
billingExport: BillingExport;
|
|
146
|
+
rateLimits?: readonly {
|
|
147
|
+
scope: "account";
|
|
148
|
+
perMinute?: number;
|
|
149
|
+
perHour?: number;
|
|
150
|
+
}[];
|
|
151
|
+
docs: {
|
|
152
|
+
pricing: string;
|
|
153
|
+
api: string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
/** Parsed balance probe. Every field is optional because providers report different subsets. */
|
|
157
|
+
export type BalanceSnapshot = {
|
|
158
|
+
remaining?: Quantity;
|
|
159
|
+
allowance?: Quantity;
|
|
160
|
+
used?: Quantity;
|
|
161
|
+
/** UTC ISO timestamp of the next allowance reset. */
|
|
162
|
+
resetsAt?: string;
|
|
163
|
+
plan?: string;
|
|
164
|
+
accountIdentity?: string;
|
|
165
|
+
rateLimitPerHour?: number;
|
|
166
|
+
};
|
|
167
|
+
/** One matchable line of a provider billing export. */
|
|
168
|
+
export type BillingLine = {
|
|
169
|
+
providerRequestId?: string;
|
|
170
|
+
operation?: OperationId;
|
|
171
|
+
occurredAt: string;
|
|
172
|
+
cost: Money;
|
|
173
|
+
/** Window of a per-day line, [from, to). */
|
|
174
|
+
window?: {
|
|
175
|
+
from: string;
|
|
176
|
+
to: string;
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
/** Per-connection choice per catalog operation; the default is metered. */
|
|
180
|
+
export type TrackingPolicy = "metered" | "passthrough";
|
|
181
|
+
/** Median of a connection's settled receipts for one operation, with its sample size. */
|
|
182
|
+
export type MeasuredPrice = (operation: OperationId) => {
|
|
183
|
+
quantity: Quantity;
|
|
184
|
+
samples: number;
|
|
185
|
+
} | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* Connection data a host resolves for the Meter: provider, selected plan, manual per-call prices,
|
|
188
|
+
* measured prices and tracking policy, all keyed by OperationId. overage is true when the host
|
|
189
|
+
* knows the plan allowance is exhausted.
|
|
190
|
+
*/
|
|
191
|
+
export type ConnectionPolicy = {
|
|
192
|
+
provider: ProviderId;
|
|
193
|
+
plan?: string;
|
|
194
|
+
manualPrices?: Readonly<Record<OperationId, Quantity>>;
|
|
195
|
+
measured?: MeasuredPrice;
|
|
196
|
+
tracking?: Readonly<Record<OperationId, TrackingPolicy>>;
|
|
197
|
+
overage?: boolean;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* What the Meter needs from a provider catalog. `@usagekit/providers` createCatalog satisfies
|
|
201
|
+
* it; the Meter does not depend on that package. operationsOf lists the operations of an
|
|
202
|
+
* enabled provider and nothing for a disabled or unknown one.
|
|
203
|
+
*/
|
|
204
|
+
export type PricingCatalog = {
|
|
205
|
+
estimate(connection: Omit<ConnectionPolicy, "tracking"> & {
|
|
206
|
+
id: string;
|
|
207
|
+
}, operation: OperationId, options: Readonly<Record<string, string>>): {
|
|
208
|
+
quantities: Quantity[];
|
|
209
|
+
source: EstimateSource;
|
|
210
|
+
priceVersion?: string;
|
|
211
|
+
};
|
|
212
|
+
operationsOf(providerId: ProviderId): readonly ProviderOperation[];
|
|
213
|
+
};
|
|
214
|
+
export type DescriptorProblem = {
|
|
215
|
+
kind: "duplicate_operation";
|
|
216
|
+
operation: OperationId;
|
|
217
|
+
} | {
|
|
218
|
+
kind: "duplicate_plan";
|
|
219
|
+
plan: string;
|
|
220
|
+
} | {
|
|
221
|
+
kind: "missing_match";
|
|
222
|
+
operation: OperationId;
|
|
223
|
+
} | {
|
|
224
|
+
kind: "unknown_price_operation";
|
|
225
|
+
row: number;
|
|
226
|
+
operation: OperationId;
|
|
227
|
+
} | {
|
|
228
|
+
kind: "unknown_price_plan";
|
|
229
|
+
row: number;
|
|
230
|
+
plan: string;
|
|
231
|
+
} | {
|
|
232
|
+
kind: "unknown_price_option";
|
|
233
|
+
row: number;
|
|
234
|
+
option: string;
|
|
235
|
+
} | {
|
|
236
|
+
kind: "invalid_price";
|
|
237
|
+
row: number;
|
|
238
|
+
field: "perUnit" | "validFrom" | "checkedAt" | "source";
|
|
239
|
+
} | {
|
|
240
|
+
kind: "unknown_probe_operation";
|
|
241
|
+
operation: OperationId;
|
|
242
|
+
} | {
|
|
243
|
+
kind: "billable_probe";
|
|
244
|
+
operation: OperationId;
|
|
245
|
+
} | {
|
|
246
|
+
kind: "unknown_price_list_operation";
|
|
247
|
+
operation: OperationId;
|
|
248
|
+
} | {
|
|
249
|
+
kind: "unknown_export_operation";
|
|
250
|
+
operation: OperationId;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Structural problems of a descriptor, in a stable order: operations, plans, prices, probe,
|
|
254
|
+
* price list, billing export. An empty list means the catalog may load it.
|
|
255
|
+
*/
|
|
256
|
+
export declare function validateDescriptor(d: ProviderDescriptor): DescriptorProblem[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const isoDate = (text) => /^\d{4}-\d{2}-\d{2}/.test(text) && Number.isFinite(Date.parse(text));
|
|
2
|
+
/**
|
|
3
|
+
* Structural problems of a descriptor, in a stable order: operations, plans, prices, probe,
|
|
4
|
+
* price list, billing export. An empty list means the catalog may load it.
|
|
5
|
+
*/
|
|
6
|
+
export function validateDescriptor(d) {
|
|
7
|
+
const problems = [];
|
|
8
|
+
const operations = new Map();
|
|
9
|
+
for (const op of d.operations) {
|
|
10
|
+
if (operations.has(op.id))
|
|
11
|
+
problems.push({ kind: "duplicate_operation", operation: op.id });
|
|
12
|
+
else
|
|
13
|
+
operations.set(op.id, op);
|
|
14
|
+
}
|
|
15
|
+
for (const op of d.operations)
|
|
16
|
+
if (op.match.length === 0 && operations.get(op.id) === op)
|
|
17
|
+
problems.push({ kind: "missing_match", operation: op.id });
|
|
18
|
+
const plans = new Set();
|
|
19
|
+
for (const plan of d.plans) {
|
|
20
|
+
if (plans.has(plan.id))
|
|
21
|
+
problems.push({ kind: "duplicate_plan", plan: plan.id });
|
|
22
|
+
plans.add(plan.id);
|
|
23
|
+
}
|
|
24
|
+
d.prices.forEach((row, index) => {
|
|
25
|
+
const op = operations.get(row.operation);
|
|
26
|
+
if (!op)
|
|
27
|
+
problems.push({ kind: "unknown_price_operation", row: index, operation: row.operation });
|
|
28
|
+
if (row.plan !== undefined && !plans.has(row.plan))
|
|
29
|
+
problems.push({ kind: "unknown_price_plan", row: index, plan: row.plan });
|
|
30
|
+
for (const key of Object.keys(row.option ?? {}))
|
|
31
|
+
if (op && !(op.optionKeys ?? []).includes(key))
|
|
32
|
+
problems.push({ kind: "unknown_price_option", row: index, option: key });
|
|
33
|
+
if (!/^\d+(\.\d+)?$/.test(row.perUnit))
|
|
34
|
+
problems.push({ kind: "invalid_price", row: index, field: "perUnit" });
|
|
35
|
+
if (!isoDate(row.validFrom))
|
|
36
|
+
problems.push({ kind: "invalid_price", row: index, field: "validFrom" });
|
|
37
|
+
if (!isoDate(row.checkedAt))
|
|
38
|
+
problems.push({ kind: "invalid_price", row: index, field: "checkedAt" });
|
|
39
|
+
if (!row.source.trim())
|
|
40
|
+
problems.push({ kind: "invalid_price", row: index, field: "source" });
|
|
41
|
+
});
|
|
42
|
+
if (d.balance) {
|
|
43
|
+
const probe = operations.get(d.balance.operation);
|
|
44
|
+
if (!probe)
|
|
45
|
+
problems.push({ kind: "unknown_probe_operation", operation: d.balance.operation });
|
|
46
|
+
else if (probe.billable)
|
|
47
|
+
problems.push({ kind: "billable_probe", operation: probe.id });
|
|
48
|
+
}
|
|
49
|
+
if (d.priceList.kind === "endpoint" && !operations.has(d.priceList.operation))
|
|
50
|
+
problems.push({ kind: "unknown_price_list_operation", operation: d.priceList.operation });
|
|
51
|
+
if (d.billingExport.operation !== undefined && !operations.has(d.billingExport.operation))
|
|
52
|
+
problems.push({ kind: "unknown_export_operation", operation: d.billingExport.operation });
|
|
53
|
+
return problems;
|
|
54
|
+
}
|
package/dist/quantity.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ import type { Quantity } from "./contracts.js";
|
|
|
2
2
|
export declare function normalizeScale(q: Quantity, scale: number): Quantity;
|
|
3
3
|
export declare function add(a: Quantity, b: Quantity): Quantity;
|
|
4
4
|
export declare function compare(a: Quantity, b: Quantity): -1 | 0 | 1;
|
|
5
|
+
/** Exact decimal text of value * 10^-scale, trailing fractional zeros trimmed, no trailing dot. */
|
|
6
|
+
export declare function format(q: Quantity): string;
|
package/dist/quantity.js
CHANGED
|
@@ -17,3 +17,9 @@ export function compare(a, b) {
|
|
|
17
17
|
const [x, y] = pair(a, b);
|
|
18
18
|
return x.value < y.value ? -1 : x.value > y.value ? 1 : 0;
|
|
19
19
|
}
|
|
20
|
+
/** Exact decimal text of value * 10^-scale, trailing fractional zeros trimmed, no trailing dot. */
|
|
21
|
+
export function format(q) {
|
|
22
|
+
const negative = q.value < 0n, digits = (negative ? -q.value : q.value).toString().padStart(q.scale + 1, "0"), whole = digits.slice(0, digits.length - q.scale), fraction = digits.slice(digits.length - q.scale).replace(/0+$/, "");
|
|
23
|
+
const text = fraction ? `${whole}.${fraction}` : whole;
|
|
24
|
+
return negative && text !== "0" ? `-${text}` : text;
|
|
25
|
+
}
|
package/dist/wire.js
CHANGED
|
@@ -23,6 +23,12 @@ export function decodeMeterJson(text) {
|
|
|
23
23
|
typeof v.costOwner === "string" &&
|
|
24
24
|
decimal(v.unknownOperations))
|
|
25
25
|
return { ...v, unknownOperations: BigInt(v.unknownOperations) };
|
|
26
|
+
if (keys.length === 3 &&
|
|
27
|
+
v.dimensions &&
|
|
28
|
+
typeof v.dimensions === "object" &&
|
|
29
|
+
["passthrough", "unpriced", "cached", "rate_limited"].includes(String(v.state)) &&
|
|
30
|
+
decimal(v.count))
|
|
31
|
+
return { ...v, count: BigInt(v.count) };
|
|
26
32
|
return value;
|
|
27
33
|
});
|
|
28
34
|
}
|