@usagekit/meter 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.
- package/dist/access.d.ts +11 -0
- package/dist/access.js +11 -0
- package/dist/meter.js +22 -10
- package/dist/validation.d.ts +4 -1
- package/dist/validation.js +31 -5
- package/package.json +3 -3
package/dist/access.d.ts
CHANGED
|
@@ -5,4 +5,15 @@ export type OwnershipResolver = (scope: Extract<BudgetScope, {
|
|
|
5
5
|
}>) => Promise<BudgetOwner | null>;
|
|
6
6
|
export declare const includes: (list: readonly string[] | "*", id: string) => boolean;
|
|
7
7
|
export declare function canRead(access: AccessContext, scope: UsageScope): boolean;
|
|
8
|
+
/** Pools and tags span principals: their figures are shared, so unreadable ones are redacted, not forbidden. */
|
|
9
|
+
export declare const isShared: (scope: BudgetScope) => scope is {
|
|
10
|
+
kind: "platform_pool";
|
|
11
|
+
namespace: string;
|
|
12
|
+
poolId: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "tag";
|
|
15
|
+
namespace: string;
|
|
16
|
+
tag: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function canReadShared(access: AccessContext, scope: BudgetScope): boolean;
|
|
8
19
|
export declare function canReadBudget(resolveOwnership: OwnershipResolver | undefined, access: AccessContext, scope: BudgetScope): Promise<boolean>;
|
package/dist/access.js
CHANGED
|
@@ -13,9 +13,20 @@ export function canRead(access, scope) {
|
|
|
13
13
|
return access.canManageBudgets || includes(access.readablePools, scope.poolId);
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/** Pools and tags span principals: their figures are shared, so unreadable ones are redacted, not forbidden. */
|
|
17
|
+
export const isShared = (scope) => scope.kind === "platform_pool" || scope.kind === "tag";
|
|
18
|
+
export function canReadShared(access, scope) {
|
|
19
|
+
if (access.namespace !== scope.namespace)
|
|
20
|
+
return false;
|
|
21
|
+
if (scope.kind === "platform_pool")
|
|
22
|
+
return canRead(access, scope);
|
|
23
|
+
return access.canManageBudgets || access.readablePrincipals === "*";
|
|
24
|
+
}
|
|
16
25
|
export async function canReadBudget(resolveOwnership, access, scope) {
|
|
17
26
|
if (access.namespace !== scope.namespace)
|
|
18
27
|
return false;
|
|
28
|
+
if (isShared(scope))
|
|
29
|
+
return canReadShared(access, scope);
|
|
19
30
|
if (scope.kind !== "connection" && scope.kind !== "access_credential")
|
|
20
31
|
return canRead(access, scope);
|
|
21
32
|
const owner = await resolveOwnership?.(scope);
|
package/dist/meter.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { InvalidInput } from "@usagekit/store";
|
|
2
2
|
import { defaultPolicy } from "./policy.js";
|
|
3
|
-
import { validate, reserveValidation, usageValidation } from "./validation.js";
|
|
4
|
-
import { canRead, canReadBudget } from "./access.js";
|
|
3
|
+
import { validate, reserveValidation, usageValidation, operationsValidation, sourceValidation, } from "./validation.js";
|
|
4
|
+
import { canRead, canReadBudget, canReadShared, isShared } from "./access.js";
|
|
5
|
+
import { defaultBudgetOrder, normalizeTags } from "@usagekit/core";
|
|
5
6
|
export function createMeter({ store, policy = defaultPolicy, clock, resolveOwnership, }) {
|
|
6
7
|
if (!Number.isFinite(clock.now().getTime()))
|
|
7
8
|
throw new Error("InvalidInput: clock");
|
|
8
|
-
if (policy.budgetOrder.length !==
|
|
9
|
-
new Set(policy.budgetOrder).size !==
|
|
10
|
-
policy.budgetOrder.some((k) => !
|
|
9
|
+
if (policy.budgetOrder.length !== defaultBudgetOrder.length ||
|
|
10
|
+
new Set(policy.budgetOrder).size !== defaultBudgetOrder.length ||
|
|
11
|
+
policy.budgetOrder.some((k) => !defaultBudgetOrder.includes(k)))
|
|
11
12
|
throw new Error("InvalidInput: budgetOrder");
|
|
12
13
|
async function safely(fn) {
|
|
13
14
|
try {
|
|
@@ -29,10 +30,12 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
|
|
|
29
30
|
const read = async (fn) => safely(async () => ({ outcome: "ok", value: await fn() }));
|
|
30
31
|
return {
|
|
31
32
|
expireReservations: (i) => command(i, () => store.expireReservations(i)),
|
|
32
|
-
reserve: async (
|
|
33
|
-
const invalid = reserveValidation(
|
|
33
|
+
reserve: async (raw) => {
|
|
34
|
+
const invalid = reserveValidation(raw);
|
|
34
35
|
if (invalid)
|
|
35
36
|
return invalid;
|
|
37
|
+
// Tags are attribution: sorted here so every adapter stores one canonical order.
|
|
38
|
+
const tags = raw.scope.tags === undefined ? undefined : normalizeTags(raw.scope.tags), i = tags ? { ...raw, scope: { ...raw.scope, tags } } : raw;
|
|
36
39
|
return safely(async () => {
|
|
37
40
|
const existing = await store.getOperation({
|
|
38
41
|
namespace: i.scope.namespace,
|
|
@@ -44,6 +47,7 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
|
|
|
44
47
|
const statuses = await store.applicableBudgets({
|
|
45
48
|
scope: i.scope,
|
|
46
49
|
surface: i.surface,
|
|
50
|
+
source: i.source,
|
|
47
51
|
units: i.estimate.map((q) => q.unit),
|
|
48
52
|
...(i.platformPools ? { platformPools: i.platformPools } : {}),
|
|
49
53
|
});
|
|
@@ -81,6 +85,14 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
|
|
|
81
85
|
return { outcome: "forbidden" };
|
|
82
86
|
return read(() => store.aggregate(q));
|
|
83
87
|
},
|
|
88
|
+
listOperations: async (access, q) => {
|
|
89
|
+
const invalid = operationsValidation(q);
|
|
90
|
+
if (invalid)
|
|
91
|
+
return invalid;
|
|
92
|
+
if (!access.canReadBillingDetail || !canRead(access, q.scope))
|
|
93
|
+
return { outcome: "forbidden" };
|
|
94
|
+
return read(() => store.listOperations(q));
|
|
95
|
+
},
|
|
84
96
|
definedBudgets: async (access, q) => {
|
|
85
97
|
const invalid = validate(q);
|
|
86
98
|
if (invalid)
|
|
@@ -90,7 +102,7 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
|
|
|
90
102
|
return read(() => store.definedBudgets(q));
|
|
91
103
|
},
|
|
92
104
|
applicableBudgets: async (access, q) => {
|
|
93
|
-
const invalid = validate(q);
|
|
105
|
+
const invalid = validate(q) ?? sourceValidation(q);
|
|
94
106
|
if (invalid)
|
|
95
107
|
return invalid;
|
|
96
108
|
if (!canRead(access, {
|
|
@@ -102,13 +114,13 @@ export function createMeter({ store, policy = defaultPolicy, clock, resolveOwner
|
|
|
102
114
|
return safely(async () => {
|
|
103
115
|
const rows = await store.applicableBudgets(q);
|
|
104
116
|
for (const status of rows) {
|
|
105
|
-
if (status.budget.scope
|
|
117
|
+
if (!isShared(status.budget.scope) &&
|
|
106
118
|
!(await canReadBudget(resolveOwnership, access, status.budget.scope)))
|
|
107
119
|
return { outcome: "forbidden" };
|
|
108
120
|
}
|
|
109
121
|
return {
|
|
110
122
|
outcome: "ok",
|
|
111
|
-
value: rows.map((s) => s.budget.scope
|
|
123
|
+
value: rows.map((s) => isShared(s.budget.scope) && !canReadShared(access, s.budget.scope)
|
|
112
124
|
? {
|
|
113
125
|
...s,
|
|
114
126
|
budget: { ...s.budget, limit: null },
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { ValidationFailure, ReserveInput, UsageQuery } from "@usagekit/core";
|
|
1
|
+
import type { ValidationFailure, ReserveInput, UsageQuery, ApplicableBudgetsQuery, OperationsQuery } from "@usagekit/core";
|
|
2
2
|
export declare function validate(value: unknown, field?: string): ValidationFailure | null;
|
|
3
|
+
/** The host sets both fields at authentication; a source outside its surface group is a forgery or a bug. */
|
|
4
|
+
export declare function sourceValidation(i: Pick<ApplicableBudgetsQuery, "surface" | "source">): ValidationFailure | null;
|
|
3
5
|
export declare function reserveValidation(i: ReserveInput): ValidationFailure | null;
|
|
6
|
+
export declare function operationsValidation(q: OperationsQuery): ValidationFailure | null;
|
|
4
7
|
export declare function usageValidation(q: UsageQuery): ValidationFailure | null;
|
package/dist/validation.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sourcesOf, normalizeTags } from "@usagekit/core";
|
|
1
2
|
export function validate(value, field = "input") {
|
|
2
3
|
const fail = (reason) => ({ outcome: "invalid", field, reason });
|
|
3
4
|
if (typeof value === "string" && !value.trim())
|
|
@@ -22,24 +23,49 @@ export function validate(value, field = "input") {
|
|
|
22
23
|
}
|
|
23
24
|
return null;
|
|
24
25
|
}
|
|
26
|
+
/** The host sets both fields at authentication; a source outside its surface group is a forgery or a bug. */
|
|
27
|
+
export function sourceValidation(i) {
|
|
28
|
+
if (i.source !== undefined && !sourcesOf(i.surface)?.includes(i.source))
|
|
29
|
+
return { outcome: "invalid", field: "source", reason: "source outside surface group" };
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
25
32
|
export function reserveValidation(i) {
|
|
26
|
-
const error = validate(i);
|
|
33
|
+
const error = validate(i) ?? sourceValidation(i);
|
|
27
34
|
if (error)
|
|
28
35
|
return error;
|
|
36
|
+
if (i.scope.tags !== undefined && !normalizeTags(i.scope.tags))
|
|
37
|
+
return {
|
|
38
|
+
outcome: "invalid",
|
|
39
|
+
field: "scope.tags",
|
|
40
|
+
reason: "1 to 16 unique tags matching the tag pattern",
|
|
41
|
+
};
|
|
29
42
|
if (new Set(i.estimate.map((q) => q.unit)).size !== i.estimate.length)
|
|
30
43
|
return { outcome: "invalid", field: "estimate", reason: "duplicate unit" };
|
|
31
44
|
return null;
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (error)
|
|
36
|
-
return error;
|
|
46
|
+
const lifecycle = ["reserved", "dispatch_intended", "pending", "settled", "released"];
|
|
47
|
+
function windowValidation(q) {
|
|
37
48
|
if (!Number.isFinite(Date.parse(q.from)) ||
|
|
38
49
|
!Number.isFinite(Date.parse(q.to)) ||
|
|
39
50
|
Date.parse(q.from) > Date.parse(q.to))
|
|
40
51
|
return { outcome: "invalid", field: "window", reason: "invalid time interval" };
|
|
41
52
|
if (q.limit !== undefined && (q.limit < 1 || q.limit > 1000))
|
|
42
53
|
return { outcome: "invalid", field: "limit", reason: "must be between 1 and 1000" };
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export function operationsValidation(q) {
|
|
57
|
+
const error = validate(q) ?? windowValidation(q);
|
|
58
|
+
if (error)
|
|
59
|
+
return error;
|
|
60
|
+
if (q.states !== undefined &&
|
|
61
|
+
(q.states.length < 1 || q.states.some((state) => !lifecycle.includes(state))))
|
|
62
|
+
return { outcome: "invalid", field: "states", reason: "one or more lifecycle states" };
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
export function usageValidation(q) {
|
|
66
|
+
const error = validate(q) ?? windowValidation(q);
|
|
67
|
+
if (error)
|
|
68
|
+
return error;
|
|
43
69
|
if (q.groupBy.includes("principal") && !["namespace", "group"].includes(q.scope.kind))
|
|
44
70
|
return {
|
|
45
71
|
outcome: "invalid",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usagekit/meter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Admission, reservations, settlement and recovery.",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"prepack": "node ../../scripts/package-license.mjs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@usagekit/core": "0.
|
|
27
|
-
"@usagekit/store": "0.
|
|
26
|
+
"@usagekit/core": "0.3.0",
|
|
27
|
+
"@usagekit/store": "0.3.0"
|
|
28
28
|
},
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"publishConfig": {
|