@usagekit/core 0.3.0 → 0.5.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/billing.d.ts +97 -0
- package/dist/billing.js +1 -0
- package/dist/contracts.d.ts +95 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/policy.d.ts +3 -1
- package/dist/policy.js +8 -1
- package/dist/providers.d.ts +256 -0
- package/dist/providers.js +54 -0
- package/dist/wire.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { BudgetAlertCrossed, Cost, FundingSource, Money, Operation, Receipt, Scope, ValidationFailure } from "./contracts.js";
|
|
2
|
+
import type { BillingLine } from "./providers.js";
|
|
3
|
+
/** Trusted historical attribution for calls the application did not observe. */
|
|
4
|
+
export type ImportAttribution = {
|
|
5
|
+
fundingSource: FundingSource;
|
|
6
|
+
costOwner: string;
|
|
7
|
+
creditAccountRef?: string;
|
|
8
|
+
customerPriceVersion?: string;
|
|
9
|
+
providerPriceVersion?: string;
|
|
10
|
+
platformPools?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
/** A complete export for one connection and half-open window. Never grants dispatch. */
|
|
13
|
+
export type BillingImportInput = {
|
|
14
|
+
scope: Scope;
|
|
15
|
+
provider: string;
|
|
16
|
+
/** Lower-case SHA-256 of the original file, computed before the atomic command. */
|
|
17
|
+
fileHash: string;
|
|
18
|
+
window: {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
};
|
|
22
|
+
/** null for the first import; the current import id for a revision. Prevents lost updates. */
|
|
23
|
+
expectedPreviousImportId: string | null;
|
|
24
|
+
attribution: ImportAttribution;
|
|
25
|
+
lines: readonly BillingLine[];
|
|
26
|
+
};
|
|
27
|
+
/** Exact comparison. Unknown ledger cost remains unknown; signed differences are never prorated. */
|
|
28
|
+
export type ReconciliationEntry = {
|
|
29
|
+
id: string;
|
|
30
|
+
importId: string;
|
|
31
|
+
scope: Scope;
|
|
32
|
+
provider: string;
|
|
33
|
+
/** Present for operation-specific aggregate evidence. */
|
|
34
|
+
operation?: string;
|
|
35
|
+
window: {
|
|
36
|
+
from: string;
|
|
37
|
+
to: string;
|
|
38
|
+
};
|
|
39
|
+
kind: "aggregate" | "import_total";
|
|
40
|
+
ledgerTotal: Cost;
|
|
41
|
+
evidenceTotal: Money;
|
|
42
|
+
/** evidenceTotal - ledgerTotal, or null when any observed cost is unknown. */
|
|
43
|
+
differenceUnits: bigint | null;
|
|
44
|
+
unknownOperations: bigint;
|
|
45
|
+
evidenceRef: string;
|
|
46
|
+
};
|
|
47
|
+
export type BillingImportRecord = {
|
|
48
|
+
id: string;
|
|
49
|
+
scope: Scope;
|
|
50
|
+
provider: string;
|
|
51
|
+
fileHash: string;
|
|
52
|
+
window: {
|
|
53
|
+
from: string;
|
|
54
|
+
to: string;
|
|
55
|
+
};
|
|
56
|
+
recordedAt: string;
|
|
57
|
+
supersedes?: string;
|
|
58
|
+
/** Projected from the import family pointer; earlier evidence remains available. */
|
|
59
|
+
supersededBy?: string;
|
|
60
|
+
matchedOperationIds: readonly string[];
|
|
61
|
+
unobservedOperationIds: readonly string[];
|
|
62
|
+
reconciliations: readonly ReconciliationEntry[];
|
|
63
|
+
alerts: readonly BudgetAlertCrossed[];
|
|
64
|
+
};
|
|
65
|
+
export type BillingImportResult = ValidationFailure | {
|
|
66
|
+
outcome: "imported";
|
|
67
|
+
replayed: boolean;
|
|
68
|
+
record: BillingImportRecord;
|
|
69
|
+
} | {
|
|
70
|
+
outcome: "rejected";
|
|
71
|
+
reason: "previous_import_conflict" | "payload_conflict" | "ambiguous_request" | "active_operation" | "operation_state" | "evidence_conflict";
|
|
72
|
+
latestImportId: string | null;
|
|
73
|
+
line?: number;
|
|
74
|
+
};
|
|
75
|
+
export type BillingImportsQuery = {
|
|
76
|
+
scope: {
|
|
77
|
+
namespace: string;
|
|
78
|
+
principal: string;
|
|
79
|
+
};
|
|
80
|
+
connection: string;
|
|
81
|
+
from: string;
|
|
82
|
+
to: string;
|
|
83
|
+
/** Include superseded import evidence; defaults to false. */
|
|
84
|
+
history?: boolean;
|
|
85
|
+
limit?: number;
|
|
86
|
+
};
|
|
87
|
+
export type BillingImportsPage = {
|
|
88
|
+
records: readonly BillingImportRecord[];
|
|
89
|
+
asOf: string;
|
|
90
|
+
truncated: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** Atomic adapter changes prepared against its transaction's operation snapshot. */
|
|
93
|
+
export type BillingOperationChange = {
|
|
94
|
+
before: Operation | null;
|
|
95
|
+
after: Operation;
|
|
96
|
+
receipt: Receipt;
|
|
97
|
+
};
|
package/dist/billing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/contracts.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BillingImportInput, BillingImportResult, BillingImportsQuery, BillingImportsPage } from "./billing.js";
|
|
1
2
|
export type ValidationFailure = {
|
|
2
3
|
outcome: "invalid";
|
|
3
4
|
field: string;
|
|
@@ -46,7 +47,7 @@ export type Scope = {
|
|
|
46
47
|
};
|
|
47
48
|
export type FundingSource = "byok" | "platform";
|
|
48
49
|
export type Surface = "app" | "programmatic";
|
|
49
|
-
export type Source = "app" | "worker" | "api" | "sdk" | "cli" | "mcp" | "proxy";
|
|
50
|
+
export type Source = "app" | "worker" | "api" | "sdk" | "cli" | "mcp" | "proxy" | "import";
|
|
50
51
|
/** Timestamps are UTC ISO strings. Resolved reservation epochs remain immutable. */
|
|
51
52
|
export type BudgetWindow = {
|
|
52
53
|
kind: "calendar_month";
|
|
@@ -163,6 +164,8 @@ export type Cost = {
|
|
|
163
164
|
*/
|
|
164
165
|
export type Receipt = {
|
|
165
166
|
id: string;
|
|
167
|
+
/** Existing receipts without this field are dispatch evidence. */
|
|
168
|
+
source?: "dispatch" | "probe" | "import";
|
|
166
169
|
supersedes?: string;
|
|
167
170
|
measurements: readonly Measurement[];
|
|
168
171
|
cost: Cost;
|
|
@@ -208,6 +211,10 @@ export type ReserveInput = {
|
|
|
208
211
|
provider: string;
|
|
209
212
|
operation: string;
|
|
210
213
|
estimate: readonly Quantity[];
|
|
214
|
+
/** Set by a Meter that resolved the estimate from a catalog; hosts may set it themselves. */
|
|
215
|
+
estimateSource?: EstimateSource;
|
|
216
|
+
/** "<provider>:<validFrom>" of the list price row behind the estimate. */
|
|
217
|
+
providerPriceVersion?: string;
|
|
211
218
|
correlationId?: string;
|
|
212
219
|
parentOperationId?: string;
|
|
213
220
|
};
|
|
@@ -260,6 +267,84 @@ export type ReserveResult = ValidationFailure | {
|
|
|
260
267
|
outcome: "conflict";
|
|
261
268
|
reason: "semantic_mismatch";
|
|
262
269
|
operation: Operation;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The connection's tracking policy is passthrough for this operation, or the catalog marks it
|
|
273
|
+
* free: counted with state passthrough, never reserved. The caller may forward the call.
|
|
274
|
+
*/
|
|
275
|
+
| {
|
|
276
|
+
outcome: "passthrough";
|
|
277
|
+
replayed: boolean;
|
|
278
|
+
operation: null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* No estimate was given and the catalog does not know the operation: counted with state
|
|
282
|
+
* unpriced, never reserved. Host policy decides whether to forward the call.
|
|
283
|
+
*/
|
|
284
|
+
| {
|
|
285
|
+
outcome: "unpriced";
|
|
286
|
+
replayed: boolean;
|
|
287
|
+
operation: null;
|
|
288
|
+
};
|
|
289
|
+
/** Where a reserve estimate came from, in resolution order. */
|
|
290
|
+
export type EstimateSource = "manual" | "measured" | "list" | "unknown";
|
|
291
|
+
/**
|
|
292
|
+
* Meter reserve input. With a catalog, estimate may be omitted: the Meter resolves it from the
|
|
293
|
+
* catalog for the connection and the request options. Store always receives a ReserveInput.
|
|
294
|
+
*/
|
|
295
|
+
export type MeterReserveInput = Omit<ReserveInput, "estimate"> & {
|
|
296
|
+
/** Omitted: the Meter resolves it through its catalog and the connection policy. */
|
|
297
|
+
estimate?: readonly Quantity[];
|
|
298
|
+
/** Request options that move the price, read from the request by the descriptor. */
|
|
299
|
+
options?: Readonly<Record<string, string>>;
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Non-operation outcomes, counted per state and never reserved. Metered requests are the
|
|
303
|
+
* operations themselves and are not counted here.
|
|
304
|
+
*/
|
|
305
|
+
export type RequestState = "passthrough" | "unpriced" | "cached" | "rate_limited";
|
|
306
|
+
/**
|
|
307
|
+
* One counted request. commandId is the replay key: an identical replay counts once and returns
|
|
308
|
+
* replayed: true; a changed payload under the same commandId is a conflict. Counts carry no
|
|
309
|
+
* content and never touch budgets.
|
|
310
|
+
*/
|
|
311
|
+
export type CountRequestInput = {
|
|
312
|
+
commandId: string;
|
|
313
|
+
scope: Scope;
|
|
314
|
+
surface: Surface;
|
|
315
|
+
source: Source;
|
|
316
|
+
provider: string;
|
|
317
|
+
operation: string;
|
|
318
|
+
state: RequestState;
|
|
319
|
+
platformPools?: readonly string[];
|
|
320
|
+
};
|
|
321
|
+
export type CountRequestResult = ValidationFailure | {
|
|
322
|
+
outcome: "counted";
|
|
323
|
+
replayed: boolean;
|
|
324
|
+
} | {
|
|
325
|
+
outcome: "conflict";
|
|
326
|
+
reason: "command_mismatch";
|
|
327
|
+
};
|
|
328
|
+
/** Requests counted at store time in [from, to), with the scope and access rules of usage. */
|
|
329
|
+
export type RequestCountsQuery = {
|
|
330
|
+
scope: UsageScope;
|
|
331
|
+
from: string;
|
|
332
|
+
to: string;
|
|
333
|
+
connection?: string;
|
|
334
|
+
groupBy: readonly ("provider" | "operation" | "connection" | "source" | "day")[];
|
|
335
|
+
/** Rows per page, 1..1000, default 1000; truncated reports more rows. */
|
|
336
|
+
limit?: number;
|
|
337
|
+
};
|
|
338
|
+
/** One row per dimension combination and state, ordered by dimensions then state. */
|
|
339
|
+
export type RequestCountRow = {
|
|
340
|
+
dimensions: Readonly<Record<string, string>>;
|
|
341
|
+
state: RequestState;
|
|
342
|
+
count: bigint;
|
|
343
|
+
};
|
|
344
|
+
export type RequestCountsPage = {
|
|
345
|
+
rows: readonly RequestCountRow[];
|
|
346
|
+
asOf: string;
|
|
347
|
+
truncated: boolean;
|
|
263
348
|
};
|
|
264
349
|
export type OperationRef = Pick<Scope, "namespace" | "principal"> & {
|
|
265
350
|
operationId: string;
|
|
@@ -432,6 +517,8 @@ export type AccessContext = {
|
|
|
432
517
|
readablePools: readonly string[] | "*";
|
|
433
518
|
canReadBillingDetail: boolean;
|
|
434
519
|
canManageBudgets: boolean;
|
|
520
|
+
/** Explicit billing write permission. Read access or budget management does not imply it. */
|
|
521
|
+
canImportBilling?: boolean;
|
|
435
522
|
};
|
|
436
523
|
/**
|
|
437
524
|
* groupBy: "principal" is allowed only for group or namespace scope.
|
|
@@ -549,8 +636,13 @@ export type ExpireReservationsResult = ValidationFailure | {
|
|
|
549
636
|
* Admission sweeps at most one default batch. Maintenance owns the remaining expired backlog.
|
|
550
637
|
*/
|
|
551
638
|
export interface Meter {
|
|
639
|
+
importBilling(access: AccessContext, input: BillingImportInput): Promise<BillingImportResult | {
|
|
640
|
+
outcome: "forbidden";
|
|
641
|
+
}>;
|
|
642
|
+
billingImports(access: AccessContext, query: BillingImportsQuery): Promise<ReadResult<BillingImportsPage>>;
|
|
552
643
|
expireReservations(input: ExpireReservationsInput): Promise<ExpireReservationsResult>;
|
|
553
|
-
reserve(input:
|
|
644
|
+
reserve(input: MeterReserveInput): Promise<ReserveResult>;
|
|
645
|
+
countRequest(input: CountRequestInput): Promise<CountRequestResult>;
|
|
554
646
|
markDispatchIntent(input: DispatchIntentInput): Promise<DispatchGrant>;
|
|
555
647
|
renewLease(input: LeaseRenewalInput): Promise<LeaseRenewal>;
|
|
556
648
|
claimForRecovery(input: RecoveryClaimInput): Promise<RecoveryClaim>;
|
|
@@ -560,6 +652,7 @@ export interface Meter {
|
|
|
560
652
|
getOperation(access: AccessContext, input: OperationRef): Promise<ReadResult<Operation | null>>;
|
|
561
653
|
usage(access: AccessContext, query: UsageQuery): Promise<ReadResult<UsagePage>>;
|
|
562
654
|
listOperations(access: AccessContext, query: OperationsQuery): Promise<ReadResult<OperationsPage>>;
|
|
655
|
+
requestCounts(access: AccessContext, query: RequestCountsQuery): Promise<ReadResult<RequestCountsPage>>;
|
|
563
656
|
definedBudgets(access: AccessContext, query: DefinedBudgetsQuery): Promise<ReadResult<readonly Budget[]>>;
|
|
564
657
|
applicableBudgets(access: AccessContext, query: ApplicableBudgetsQuery): Promise<ReadResult<readonly BudgetStatus[]>>;
|
|
565
658
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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, } 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
5
|
export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
|
|
@@ -7,3 +8,6 @@ export type { ValidationFailure, AdmissionPolicy, BudgetOwner } from "./contract
|
|
|
7
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";
|
|
13
|
+
export type { ImportAttribution, BillingImportInput, ReconciliationEntry, BillingImportRecord, BillingImportResult, BillingImportsQuery, BillingImportsPage, BillingOperationChange, } from "./billing.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export { requestStates } from "./policy.js";
|
|
1
2
|
export { resolveWindow } from "./windows.js";
|
|
2
3
|
export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
|
|
3
4
|
export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
|
|
4
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/policy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BudgetScope, Source, Surface } from "./contracts.js";
|
|
1
|
+
import type { BudgetScope, RequestState, Source, Surface } from "./contracts.js";
|
|
2
2
|
/** Single source of the default admission order; hosts may permute it, never shorten it. */
|
|
3
3
|
export declare const defaultBudgetOrder: readonly BudgetScope["kind"][];
|
|
4
4
|
export declare const tagPattern: RegExp;
|
|
@@ -8,3 +8,5 @@ export declare function normalizeTags(tags: readonly string[]): string[] | null;
|
|
|
8
8
|
export declare const sourcesOf: (surface: Surface) => readonly Source[];
|
|
9
9
|
/** True when a budget surface value bounds an operation with this surface and source. */
|
|
10
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
|
@@ -19,7 +19,7 @@ export function normalizeTags(tags) {
|
|
|
19
19
|
}
|
|
20
20
|
const surfaceSources = Object.freeze({
|
|
21
21
|
app: Object.freeze(["app", "worker"]),
|
|
22
|
-
programmatic: Object.freeze(["api", "sdk", "cli", "mcp", "proxy"]),
|
|
22
|
+
programmatic: Object.freeze(["api", "sdk", "cli", "mcp", "proxy", "import"]),
|
|
23
23
|
});
|
|
24
24
|
/** Sources a surface budget covers. Every Source belongs to exactly one Surface. */
|
|
25
25
|
export const sourcesOf = (surface) => surfaceSources[surface];
|
|
@@ -27,3 +27,10 @@ export const sourcesOf = (surface) => surfaceSources[surface];
|
|
|
27
27
|
export const surfaceMatches = (budgetSurface, surface, source) => budgetSurface === "any" ||
|
|
28
28
|
budgetSurface === surface ||
|
|
29
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/wire.js
CHANGED
|
@@ -23,6 +23,26 @@ 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) };
|
|
32
|
+
if (typeof v.id === "string" &&
|
|
33
|
+
typeof v.importId === "string" &&
|
|
34
|
+
["aggregate", "import_total"].includes(String(v.kind)) &&
|
|
35
|
+
v.scope &&
|
|
36
|
+
v.window &&
|
|
37
|
+
v.ledgerTotal &&
|
|
38
|
+
v.evidenceTotal &&
|
|
39
|
+
decimal(v.unknownOperations) &&
|
|
40
|
+
(v.differenceUnits === null || decimal(v.differenceUnits)))
|
|
41
|
+
return {
|
|
42
|
+
...v,
|
|
43
|
+
unknownOperations: BigInt(v.unknownOperations),
|
|
44
|
+
differenceUnits: v.differenceUnits === null ? null : BigInt(v.differenceUnits),
|
|
45
|
+
};
|
|
26
46
|
return value;
|
|
27
47
|
});
|
|
28
48
|
}
|