@usagekit/core 0.1.0 → 0.3.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.
@@ -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
- onExceed: "block" | "warn";
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 "any".
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
  */
@@ -205,11 +245,13 @@ export type ReserveResult = ValidationFailure | {
205
245
  replayed: false;
206
246
  operation: Operation;
207
247
  warnings: readonly AllowanceExceeded[];
248
+ alerts: readonly BudgetAlertCrossed[];
208
249
  } | {
209
250
  outcome: "reserved";
210
251
  replayed: true;
211
252
  operation: Operation;
212
253
  warnings: readonly AllowanceExceeded[];
254
+ alerts: readonly BudgetAlertCrossed[];
213
255
  } | {
214
256
  outcome: "exceeded";
215
257
  exceeded: AllowanceExceeded;
@@ -341,6 +383,7 @@ export type SettleResult = ValidationFailure | {
341
383
  outcome: "settled";
342
384
  replayed: boolean;
343
385
  operation: Operation;
386
+ alerts: readonly BudgetAlertCrossed[];
344
387
  } | {
345
388
  outcome: "rejected";
346
389
  reason: "not_holder" | "lease_expired" | "version_conflict" | "invalid_state" | "receipt_conflict";
@@ -390,14 +433,18 @@ export type AccessContext = {
390
433
  canReadBillingDetail: boolean;
391
434
  canManageBudgets: boolean;
392
435
  };
393
- /** groupBy: "principal" is allowed only for group or namespace scope. */
436
+ /**
437
+ * groupBy: "principal" is allowed only for group or namespace scope.
438
+ * "platform_pool" and "tag" explode one row per pool or tag of an operation, with an
439
+ * empty dimension when the operation has none; those rows are not additive across tags.
440
+ */
394
441
  export type UsageQuery = {
395
442
  scope: UsageScope;
396
443
  from: string;
397
444
  to: string;
398
445
  connection?: string;
399
446
  units: readonly string[];
400
- groupBy: readonly ("principal" | "provider" | "operation" | "surface" | "source" | "connection" | "day" | "access_credential" | "platform_pool")[];
447
+ groupBy: readonly ("principal" | "provider" | "operation" | "surface" | "source" | "connection" | "day" | "access_credential" | "platform_pool" | "funding_source" | "tag")[];
401
448
  cursor?: string;
402
449
  limit?: number;
403
450
  };
@@ -415,6 +462,27 @@ export type UsagePage = {
415
462
  watermark: string;
416
463
  nextCursor?: string;
417
464
  };
465
+ /**
466
+ * Operations whose createdAt lies in [from, to), in one lifecycle state of states (all when
467
+ * omitted), ordered by createdAt then operationId. Same scope and access rules as usage.
468
+ * Pagination mirrors usage: the first page fixes membership and order at its watermark; later
469
+ * pages take the cursor with the unchanged query and return each operation as currently stored.
470
+ */
471
+ export type OperationsQuery = {
472
+ scope: UsageScope;
473
+ from: string;
474
+ to: string;
475
+ states?: readonly LifecycleState[];
476
+ connection?: string;
477
+ cursor?: string;
478
+ limit?: number;
479
+ };
480
+ export type OperationsPage = {
481
+ operations: readonly Operation[];
482
+ asOf: string;
483
+ watermark: string;
484
+ nextCursor?: string;
485
+ };
418
486
  /** Budgets defined for exactly this scope, regardless of any operation. */
419
487
  export type DefinedBudgetsQuery = {
420
488
  scope: BudgetScope;
@@ -428,6 +496,8 @@ export type DefinedBudgetsQuery = {
428
496
  export type ApplicableBudgetsQuery = {
429
497
  scope: Scope;
430
498
  surface: Surface;
499
+ /** Include budgets bound to exactly this source. Omitted lists surface and any budgets only. */
500
+ source?: Source;
431
501
  units: readonly string[];
432
502
  platformPools?: readonly string[];
433
503
  };
@@ -451,10 +521,12 @@ export type ReadResult<T> = ValidationFailure | {
451
521
  } | {
452
522
  outcome: "forbidden";
453
523
  };
454
- /** Data-only budget denial. A P1 implementation may wrap it in a runtime error class. */
524
+ /** Data-only budget denial or warning. A P1 implementation may wrap it in a runtime error class. */
455
525
  export interface AllowanceExceeded {
456
526
  readonly code: "allowance_exceeded";
457
527
  readonly budget: Budget;
528
+ /** Which bound was crossed: limit warns on allow budgets and denies on block budgets; hardLimit always denies. */
529
+ readonly boundary: "limit" | "hardLimit";
458
530
  readonly used: Quantity;
459
531
  readonly reserved: Quantity;
460
532
  readonly resetsAt: string | null;
@@ -487,6 +559,7 @@ export interface Meter {
487
559
  releaseUndispatched(input: ReleaseInput): Promise<ReleaseResult>;
488
560
  getOperation(access: AccessContext, input: OperationRef): Promise<ReadResult<Operation | null>>;
489
561
  usage(access: AccessContext, query: UsageQuery): Promise<ReadResult<UsagePage>>;
562
+ listOperations(access: AccessContext, query: OperationsQuery): Promise<ReadResult<OperationsPage>>;
490
563
  definedBudgets(access: AccessContext, query: DefinedBudgetsQuery): Promise<ReadResult<readonly Budget[]>>;
491
564
  applicableBudgets(access: AccessContext, query: ApplicableBudgetsQuery): Promise<ReadResult<readonly BudgetStatus[]>>;
492
565
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
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, } from "./contracts.js";
2
2
  export { resolveWindow } from "./windows.js";
3
3
  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";
4
+ export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
5
+ export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
6
6
  export type { ValidationFailure, AdmissionPolicy, BudgetOwner } from "./contracts.js";
7
- export { defaultBudgetOrder } from "./policy.js";
7
+ export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
8
8
  export type { ExpireReservationsInput, ExpireReservationsResult } from "./contracts.js";
9
9
  export { decodeMeterJson } from "./wire.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  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";
2
+ export { fromDecimalString, toDecimalString, formatMoney, add as addMoney, sub as subtractMoney, compare as compareMoney, } from "./money.js";
3
+ export { normalizeScale, add as addQuantity, compare as compareQuantity, format as formatQuantity, } from "./quantity.js";
4
+ export { defaultBudgetOrder, sourcesOf, surfaceMatches, tagPattern, normalizeTags, } from "./policy.js";
5
5
  export { decodeMeterJson } from "./wire.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,10 @@
1
- import type { BudgetScope } from "./contracts.js";
1
+ import type { BudgetScope, 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;
package/dist/policy.js CHANGED
@@ -1,7 +1,29 @@
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);
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagekit/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Exact quantities, ownership and pure domain types.",