@usagekit/providers 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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/dist/budget.d.ts +15 -0
- package/dist/budget.js +50 -0
- package/dist/catalog.d.ts +59 -0
- package/dist/catalog.js +149 -0
- package/dist/dataforseo/descriptor.d.ts +10 -0
- package/dist/dataforseo/descriptor.js +106 -0
- package/dist/dataforseo/extractors.d.ts +8 -0
- package/dist/dataforseo/extractors.js +145 -0
- package/dist/dataforseo/index.d.ts +4 -0
- package/dist/dataforseo/index.js +5 -0
- package/dist/eslint.d.ts +8 -0
- package/dist/eslint.js +70 -0
- package/dist/execute.d.ts +36 -0
- package/dist/execute.js +81 -0
- package/dist/fixtures.d.ts +45 -0
- package/dist/fixtures.js +96 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +10 -0
- package/dist/match.d.ts +7 -0
- package/dist/match.js +55 -0
- package/dist/pointer.d.ts +2 -0
- package/dist/pointer.js +13 -0
- package/dist/prices.d.ts +11 -0
- package/dist/prices.js +25 -0
- package/dist/receipts.d.ts +10 -0
- package/dist/receipts.js +32 -0
- package/dist/serpapi/descriptor.d.ts +8 -0
- package/dist/serpapi/descriptor.js +97 -0
- package/dist/serpapi/extractors.d.ts +2 -0
- package/dist/serpapi/extractors.js +46 -0
- package/dist/serpapi/index.d.ts +3 -0
- package/dist/serpapi/index.js +4 -0
- package/dist/testing/conformance.d.ts +11 -0
- package/dist/testing/conformance.js +33 -0
- package/dist/testing/example.d.ts +10 -0
- package/dist/testing/example.js +146 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +3 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.js +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Measurement, Money, Quantity } from "@usagekit/core";
|
|
2
|
+
/** Exact quantity of a nonnegative decimal string in a unit: "0.0150" cents is 150 at scale 4. */
|
|
3
|
+
export declare function decimalQuantity(text: string, unit: string): Quantity;
|
|
4
|
+
/** Exact decimal text of a JSON number, rounded to at most `digits` fractional digits. */
|
|
5
|
+
export declare function numberText(n: number, digits: number): string;
|
|
6
|
+
/** Money from a cents decimal string with up to four fractional digits. */
|
|
7
|
+
export declare function moneyFromCents(cents: string): Money;
|
|
8
|
+
/** Money from a USD amount reported as a JSON number; 1 USD is 1_000_000 money units. */
|
|
9
|
+
export declare function moneyFromDollars(dollars: number): Money;
|
|
10
|
+
export declare const requestsMeasured: (count?: bigint) => Measurement;
|
package/dist/receipts.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const decimalPattern = /^(\d+)(?:\.(\d+))?$/;
|
|
2
|
+
/** Exact quantity of a nonnegative decimal string in a unit: "0.0150" cents is 150 at scale 4. */
|
|
3
|
+
export function decimalQuantity(text, unit) {
|
|
4
|
+
const m = decimalPattern.exec(text);
|
|
5
|
+
if (!m || (m[2]?.length ?? 0) > 18)
|
|
6
|
+
throw new Error(`InvalidDecimal: ${text}`);
|
|
7
|
+
return { value: BigInt(m[1] + (m[2] ?? "")), scale: m[2]?.length ?? 0, unit };
|
|
8
|
+
}
|
|
9
|
+
/** Exact decimal text of a JSON number, rounded to at most `digits` fractional digits. */
|
|
10
|
+
export function numberText(n, digits) {
|
|
11
|
+
if (!Number.isFinite(n) || n < 0)
|
|
12
|
+
throw new Error(`InvalidDecimal: ${n}`);
|
|
13
|
+
const fixed = n.toFixed(digits);
|
|
14
|
+
return fixed.includes(".") ? fixed.replace(/0+$/, "").replace(/\.$/, "") : fixed;
|
|
15
|
+
}
|
|
16
|
+
/** Money from a cents decimal string with up to four fractional digits. */
|
|
17
|
+
export function moneyFromCents(cents) {
|
|
18
|
+
const q = decimalQuantity(cents, "cents");
|
|
19
|
+
if (q.scale > 4)
|
|
20
|
+
throw new Error(`InvalidDecimal: ${cents}`);
|
|
21
|
+
return { units: q.value * 10n ** BigInt(4 - q.scale), currency: "USD" };
|
|
22
|
+
}
|
|
23
|
+
/** Money from a USD amount reported as a JSON number; 1 USD is 1_000_000 money units. */
|
|
24
|
+
export function moneyFromDollars(dollars) {
|
|
25
|
+
const q = decimalQuantity(numberText(dollars, 6), "usd");
|
|
26
|
+
return { units: q.value * 10n ** BigInt(6 - q.scale), currency: "USD" };
|
|
27
|
+
}
|
|
28
|
+
export const requestsMeasured = (count = 1n) => ({
|
|
29
|
+
unit: "requests",
|
|
30
|
+
quantity: { value: count, scale: 0, unit: "requests" },
|
|
31
|
+
certainty: "measured",
|
|
32
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProviderDescriptor } from "@usagekit/core";
|
|
2
|
+
export declare const pricing = "https://serpapi.com/pricing";
|
|
3
|
+
/**
|
|
4
|
+
* Provider B: no per-call cost, monthly plans with a search allowance, query-parameter auth.
|
|
5
|
+
* A search costs one plan unit; past the allowance it draws one extra credit. Cloud and
|
|
6
|
+
* enterprise tiers are left to manual prices on the connection.
|
|
7
|
+
*/
|
|
8
|
+
export declare const descriptor: ProviderDescriptor;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export const pricing = "https://serpapi.com/pricing";
|
|
2
|
+
const checked = "2026-09-25";
|
|
3
|
+
const monthly = [
|
|
4
|
+
["free", "Free (250 searches per month)", 250n],
|
|
5
|
+
["starter", "Starter (25 USD, 1,000 searches per month)", 1000n],
|
|
6
|
+
["developer", "Developer (75 USD, 5,000 searches per month)", 5000n],
|
|
7
|
+
["production", "Production (150 USD, 15,000 searches per month)", 15000n],
|
|
8
|
+
["bigdata", "Big Data (275 USD, 30,000 searches per month)", 30000n],
|
|
9
|
+
["searcher", "Searcher (725 USD, 100,000 searches per month)", 100000n],
|
|
10
|
+
["volume", "Volume (1,475 USD, 250,000 searches per month)", 250000n],
|
|
11
|
+
["infrastructure", "Infrastructure (2,750 USD, 500,000 searches per month)", 500000n],
|
|
12
|
+
];
|
|
13
|
+
const row = (extra) => ({
|
|
14
|
+
validFrom: checked,
|
|
15
|
+
operation: "search",
|
|
16
|
+
unit: "units",
|
|
17
|
+
perUnit: "1",
|
|
18
|
+
source: pricing,
|
|
19
|
+
checkedAt: checked,
|
|
20
|
+
...extra,
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Provider B: no per-call cost, monthly plans with a search allowance, query-parameter auth.
|
|
24
|
+
* A search costs one plan unit; past the allowance it draws one extra credit. Cloud and
|
|
25
|
+
* enterprise tiers are left to manual prices on the connection.
|
|
26
|
+
*/
|
|
27
|
+
export const descriptor = {
|
|
28
|
+
id: "serpapi",
|
|
29
|
+
label: "SerpApi",
|
|
30
|
+
upstream: "https://serpapi.com",
|
|
31
|
+
auth: { kind: "query", param: "api_key" },
|
|
32
|
+
billing: { unit: "units", cycle: "monthly_plan" },
|
|
33
|
+
plans: [
|
|
34
|
+
...monthly.map(([id, label, searches]) => ({
|
|
35
|
+
id,
|
|
36
|
+
label,
|
|
37
|
+
cycle: "monthly_plan",
|
|
38
|
+
allowance: { unit: "units", value: searches },
|
|
39
|
+
allowanceMode: "hard",
|
|
40
|
+
})),
|
|
41
|
+
{ id: "extra_credits", label: "Extra credits", cycle: "prepaid", allowanceMode: "hard" },
|
|
42
|
+
],
|
|
43
|
+
prices: [
|
|
44
|
+
...monthly.map(([plan]) => row({ plan })),
|
|
45
|
+
row({ plan: "extra_credits" }),
|
|
46
|
+
row({ overage: true }),
|
|
47
|
+
],
|
|
48
|
+
priceList: { kind: "static" },
|
|
49
|
+
operations: [
|
|
50
|
+
{
|
|
51
|
+
id: "search",
|
|
52
|
+
label: "Search",
|
|
53
|
+
billable: true,
|
|
54
|
+
match: [
|
|
55
|
+
{ method: "GET", path: "/search" },
|
|
56
|
+
{ method: "GET", path: "/search.json" },
|
|
57
|
+
],
|
|
58
|
+
costEvidence: "none",
|
|
59
|
+
cacheable: {
|
|
60
|
+
keyParams: ["engine", "q", "location", "hl", "gl", "device"],
|
|
61
|
+
defaultTtlMs: 3600000,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "account",
|
|
66
|
+
label: "Account",
|
|
67
|
+
billable: false,
|
|
68
|
+
match: [
|
|
69
|
+
{ method: "GET", path: "/account" },
|
|
70
|
+
{ method: "GET", path: "/account.json" },
|
|
71
|
+
],
|
|
72
|
+
costEvidence: "none",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "locations",
|
|
76
|
+
label: "Locations",
|
|
77
|
+
billable: false,
|
|
78
|
+
match: [{ method: "GET", path: "/locations.json" }],
|
|
79
|
+
costEvidence: "none",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
balance: {
|
|
83
|
+
operation: "account",
|
|
84
|
+
fields: {
|
|
85
|
+
remaining: "/plan_searches_left",
|
|
86
|
+
remainingUnit: "units",
|
|
87
|
+
allowance: "/searches_per_month",
|
|
88
|
+
used: "/this_month_usage",
|
|
89
|
+
resetsAt: "/plan_renewal_date",
|
|
90
|
+
plan: "/plan_id",
|
|
91
|
+
accountIdentity: "/account_email",
|
|
92
|
+
rateLimitPerHour: "/account_rate_limit_per_hour",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
billingExport: { kind: "none", granularity: "per-day", matchKey: "window" },
|
|
96
|
+
docs: { pricing, api: "https://serpapi.com/search-api" },
|
|
97
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { listEstimate } from "../prices.js";
|
|
2
|
+
import { pointer } from "../pointer.js";
|
|
3
|
+
import { requestsMeasured } from "../receipts.js";
|
|
4
|
+
const zero = { units: 0n, currency: "USD" };
|
|
5
|
+
const units = (value) => ({ value, scale: 0, unit: "units" });
|
|
6
|
+
export const extractors = {
|
|
7
|
+
estimate: listEstimate,
|
|
8
|
+
options: () => ({}),
|
|
9
|
+
/**
|
|
10
|
+
* The provider reports no cost per call. A search settles as one estimated plan unit with zero
|
|
11
|
+
* marginal money (the plan fee is not a per-call charge); reconciliation compares
|
|
12
|
+
* this_month_usage deltas. Errored searches consume no search. Free endpoints cost nothing.
|
|
13
|
+
*/
|
|
14
|
+
extract: (operation, _request, response, body) => {
|
|
15
|
+
const id = extractors.requestId(body);
|
|
16
|
+
const failed = response.status >= 400 || typeof pointer(body, "/error") === "string";
|
|
17
|
+
const common = { ...(id ? { providerRequestId: id } : {}), cached: false, failed };
|
|
18
|
+
if (!operation.billable)
|
|
19
|
+
return {
|
|
20
|
+
measurements: [requestsMeasured()],
|
|
21
|
+
cost: { certainty: "measured", money: zero },
|
|
22
|
+
...common,
|
|
23
|
+
};
|
|
24
|
+
return failed
|
|
25
|
+
? {
|
|
26
|
+
measurements: [
|
|
27
|
+
{ unit: "units", quantity: units(0n), certainty: "measured" },
|
|
28
|
+
requestsMeasured(),
|
|
29
|
+
],
|
|
30
|
+
cost: { certainty: "measured", money: zero },
|
|
31
|
+
...common,
|
|
32
|
+
}
|
|
33
|
+
: {
|
|
34
|
+
measurements: [
|
|
35
|
+
{ unit: "units", quantity: units(1n), certainty: "estimated" },
|
|
36
|
+
requestsMeasured(),
|
|
37
|
+
],
|
|
38
|
+
cost: { certainty: "estimated", money: zero },
|
|
39
|
+
...common,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
requestId: (body) => {
|
|
43
|
+
const id = pointer(body, "/search_metadata/id");
|
|
44
|
+
return typeof id === "string" && id ? id : undefined;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProviderDescriptor } from "@usagekit/core";
|
|
2
|
+
import type { Extractors } from "../types.js";
|
|
3
|
+
import type { FixtureDirectory } from "../fixtures.js";
|
|
4
|
+
/**
|
|
5
|
+
* Contract tests for one descriptor: it validates, every advertised operation has at least one
|
|
6
|
+
* fixture, and every fixture matches its operation and yields the expected receipt or parsed
|
|
7
|
+
* value. fixturesDir maps each fixture path to its parsed JSON, for example
|
|
8
|
+
* `import.meta.glob("./fixtures/**\/*.json", { eager: true, import: "default" })`, because web
|
|
9
|
+
* runtime packages cannot read directories.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runDescriptorConformance(descriptor: ProviderDescriptor, extractors: Extractors, fixturesDir: FixtureDirectory): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { validateDescriptor } from "@usagekit/core";
|
|
3
|
+
import { createCatalog } from "../catalog.js";
|
|
4
|
+
import { loadFixtures, missingFixtures, verifyFixture } from "../fixtures.js";
|
|
5
|
+
/**
|
|
6
|
+
* Contract tests for one descriptor: it validates, every advertised operation has at least one
|
|
7
|
+
* fixture, and every fixture matches its operation and yields the expected receipt or parsed
|
|
8
|
+
* value. fixturesDir maps each fixture path to its parsed JSON, for example
|
|
9
|
+
* `import.meta.glob("./fixtures/**\/*.json", { eager: true, import: "default" })`, because web
|
|
10
|
+
* runtime packages cannot read directories.
|
|
11
|
+
*/
|
|
12
|
+
export function runDescriptorConformance(descriptor, extractors, fixturesDir) {
|
|
13
|
+
describe(`descriptor ${descriptor.id}`, () => {
|
|
14
|
+
const fixtures = loadFixtures(fixturesDir);
|
|
15
|
+
const catalog = createCatalog({
|
|
16
|
+
providers: [{ descriptor, extractors }],
|
|
17
|
+
enabled: [descriptor.id],
|
|
18
|
+
});
|
|
19
|
+
test("validates without problems", () => {
|
|
20
|
+
expect(validateDescriptor(descriptor)).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
test("every advertised operation has a fixture", () => {
|
|
23
|
+
expect(missingFixtures(descriptor, fixtures)).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
test("every fixture belongs to this descriptor", () => {
|
|
26
|
+
expect(fixtures.filter((f) => f.provider !== descriptor.id).map((f) => f.path)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
for (const fixture of fixtures)
|
|
29
|
+
test(`fixture ${fixture.operation}/${fixture.path.split("/").at(-1)}`, () => {
|
|
30
|
+
expect(verifyFixture(catalog, fixture)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProviderDescriptor } from "@usagekit/core";
|
|
2
|
+
import type { Extractors, ProviderModule } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A synthetic descriptor that exercises every catalog rule: a base path, match precedence, plan
|
|
5
|
+
* and option axes, an overage row, a free probe. It is the template for new descriptors and is
|
|
6
|
+
* never enabled by hosts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const exampleDescriptor: ProviderDescriptor;
|
|
9
|
+
export declare const exampleExtractors: Extractors;
|
|
10
|
+
export declare const exampleProvider: ProviderModule;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { listEstimate } from "../prices.js";
|
|
2
|
+
import { moneyFromCents, requestsMeasured } from "../receipts.js";
|
|
3
|
+
const source = "https://example.com/pricing";
|
|
4
|
+
/**
|
|
5
|
+
* A synthetic descriptor that exercises every catalog rule: a base path, match precedence, plan
|
|
6
|
+
* and option axes, an overage row, a free probe. It is the template for new descriptors and is
|
|
7
|
+
* never enabled by hosts.
|
|
8
|
+
*/
|
|
9
|
+
export const exampleDescriptor = {
|
|
10
|
+
id: "example",
|
|
11
|
+
label: "Example provider",
|
|
12
|
+
upstream: "https://api.example.com/v1",
|
|
13
|
+
auth: { kind: "header", name: "X-Api-Key" },
|
|
14
|
+
billing: { unit: "units", cycle: "monthly_plan" },
|
|
15
|
+
plans: [
|
|
16
|
+
{
|
|
17
|
+
id: "basic",
|
|
18
|
+
label: "Basic",
|
|
19
|
+
cycle: "monthly_plan",
|
|
20
|
+
allowance: { unit: "units", value: 1000n },
|
|
21
|
+
allowanceMode: "hard",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "pro",
|
|
25
|
+
label: "Pro",
|
|
26
|
+
cycle: "monthly_plan",
|
|
27
|
+
allowance: { unit: "units", value: 5000n },
|
|
28
|
+
allowanceMode: "soft",
|
|
29
|
+
},
|
|
30
|
+
{ id: "credits", label: "Credits", cycle: "prepaid", allowanceMode: "hard" },
|
|
31
|
+
],
|
|
32
|
+
prices: [
|
|
33
|
+
{ validFrom: "2026-01-01", operation: "search", plan: "basic", unit: "units", perUnit: "1" },
|
|
34
|
+
{
|
|
35
|
+
validFrom: "2026-01-01",
|
|
36
|
+
operation: "search",
|
|
37
|
+
plan: "basic",
|
|
38
|
+
option: { priority: "high" },
|
|
39
|
+
unit: "units",
|
|
40
|
+
perUnit: "2",
|
|
41
|
+
},
|
|
42
|
+
{ validFrom: "2026-01-01", operation: "search", plan: "pro", unit: "units", perUnit: "1" },
|
|
43
|
+
{
|
|
44
|
+
validFrom: "2026-01-01",
|
|
45
|
+
operation: "search",
|
|
46
|
+
plan: "pro",
|
|
47
|
+
overage: true,
|
|
48
|
+
unit: "cents",
|
|
49
|
+
perUnit: "0.5000",
|
|
50
|
+
},
|
|
51
|
+
{ validFrom: "2026-01-01", operation: "search", plan: "credits", unit: "units", perUnit: "1" },
|
|
52
|
+
{ validFrom: "2026-01-01", operation: "search.images", unit: "cents", perUnit: "0.30" },
|
|
53
|
+
{ validFrom: "2026-10-01", operation: "search.images", unit: "cents", perUnit: "0.40" },
|
|
54
|
+
{ validFrom: "2026-01-01", operation: "items.get", unit: "cents", perUnit: "0.1" },
|
|
55
|
+
{ validFrom: "2026-01-01", operation: "items.special", unit: "cents", perUnit: "0.2" },
|
|
56
|
+
].map((row) => ({ ...row, source, checkedAt: "2026-09-25" })),
|
|
57
|
+
priceList: { kind: "static" },
|
|
58
|
+
operations: [
|
|
59
|
+
{
|
|
60
|
+
id: "search",
|
|
61
|
+
label: "Web search",
|
|
62
|
+
billable: true,
|
|
63
|
+
match: [{ method: "GET", path: "/search" }],
|
|
64
|
+
optionKeys: ["priority"],
|
|
65
|
+
costEvidence: "response",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "search.images",
|
|
69
|
+
label: "Image search",
|
|
70
|
+
billable: true,
|
|
71
|
+
match: [{ method: "GET", path: "/search", query: { engine: "images" } }],
|
|
72
|
+
costEvidence: "response",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "items.get",
|
|
76
|
+
label: "Item",
|
|
77
|
+
billable: true,
|
|
78
|
+
match: [{ method: "GET", path: "/items/:id" }],
|
|
79
|
+
costEvidence: "response",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "items.special",
|
|
83
|
+
label: "Special item",
|
|
84
|
+
billable: true,
|
|
85
|
+
match: [{ method: "GET", path: "/items/special" }],
|
|
86
|
+
costEvidence: "response",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "files",
|
|
90
|
+
label: "Files",
|
|
91
|
+
billable: true,
|
|
92
|
+
match: [{ method: "ANY", path: "/files/*" }],
|
|
93
|
+
costEvidence: "none",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "account",
|
|
97
|
+
label: "Account",
|
|
98
|
+
billable: false,
|
|
99
|
+
match: [{ method: "GET", path: "/account" }],
|
|
100
|
+
costEvidence: "none",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
balance: {
|
|
104
|
+
operation: "account",
|
|
105
|
+
fields: {
|
|
106
|
+
remaining: "/searches_left",
|
|
107
|
+
remainingUnit: "units",
|
|
108
|
+
allowance: "/searches_per_month",
|
|
109
|
+
used: "/used",
|
|
110
|
+
resetsAt: "/renews_on",
|
|
111
|
+
plan: "/plan",
|
|
112
|
+
accountIdentity: "/email",
|
|
113
|
+
rateLimitPerHour: "/hourly_limit",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
billingExport: { kind: "none", granularity: "per-request", matchKey: "providerRequestId" },
|
|
117
|
+
docs: { pricing: source, api: "https://example.com/docs" },
|
|
118
|
+
};
|
|
119
|
+
export const exampleExtractors = {
|
|
120
|
+
estimate: listEstimate,
|
|
121
|
+
options: (operation, request) => {
|
|
122
|
+
const priority = new URL(request.url).searchParams.get("priority");
|
|
123
|
+
return operation.optionKeys?.includes("priority") && priority ? { priority } : {};
|
|
124
|
+
},
|
|
125
|
+
extract: (_operation, _request, response, body) => {
|
|
126
|
+
const b = (body ?? {});
|
|
127
|
+
const id = typeof b.id === "string" ? b.id : undefined;
|
|
128
|
+
return {
|
|
129
|
+
measurements: [requestsMeasured()],
|
|
130
|
+
cost: typeof b.cost_cents === "string"
|
|
131
|
+
? { certainty: "measured", money: moneyFromCents(b.cost_cents) }
|
|
132
|
+
: { certainty: "unknown", money: null },
|
|
133
|
+
...(id ? { providerRequestId: id } : {}),
|
|
134
|
+
cached: false,
|
|
135
|
+
failed: response.status >= 400,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
requestId: (body) => {
|
|
139
|
+
const id = body?.id;
|
|
140
|
+
return typeof id === "string" ? id : undefined;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
export const exampleProvider = {
|
|
144
|
+
descriptor: exampleDescriptor,
|
|
145
|
+
extractors: exampleExtractors,
|
|
146
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { BalanceSnapshot, BillingLine, PriceRow, ProviderDescriptor, ProviderOperation, Quantity, Receipt } from "@usagekit/core";
|
|
2
|
+
/**
|
|
3
|
+
* A provider request as data. The body is already parsed (JSON value or text), so extractors
|
|
4
|
+
* stay synchronous and never consume a stream.
|
|
5
|
+
*/
|
|
6
|
+
export type ProviderRequest = {
|
|
7
|
+
method: string;
|
|
8
|
+
/** Absolute URL, or a path resolved against the descriptor's upstream. */
|
|
9
|
+
url: string;
|
|
10
|
+
headers?: Readonly<Record<string, string>>;
|
|
11
|
+
body?: unknown;
|
|
12
|
+
};
|
|
13
|
+
export type ProviderResponse = {
|
|
14
|
+
status: number;
|
|
15
|
+
headers?: Readonly<Record<string, string>>;
|
|
16
|
+
};
|
|
17
|
+
/** A receipt without identity and timestamps; the host wrapper adds id, occurredAt, recordedAt. */
|
|
18
|
+
export type ReceiptDraft = Omit<Receipt, "id" | "supersedes" | "occurredAt" | "recordedAt">;
|
|
19
|
+
export type EstimateInput = {
|
|
20
|
+
operation: ProviderOperation;
|
|
21
|
+
options: Readonly<Record<string, string>>;
|
|
22
|
+
plan?: string;
|
|
23
|
+
/** Past the plan allowance: only overage rows apply. */
|
|
24
|
+
overage?: boolean;
|
|
25
|
+
/** Rows valid at the catalog date, in descriptor order. */
|
|
26
|
+
prices: readonly PriceRow[];
|
|
27
|
+
};
|
|
28
|
+
/** The only code in a descriptor. Every function is pure and synchronous. */
|
|
29
|
+
export type Extractors = {
|
|
30
|
+
/** Estimate before the call from the matched operation, options and connection plan. Empty means no price. */
|
|
31
|
+
estimate(input: EstimateInput): Quantity[];
|
|
32
|
+
/** Options that select a price row, read from the request. Options are request data, never guessed. */
|
|
33
|
+
options(operation: ProviderOperation, request: ProviderRequest): Record<string, string>;
|
|
34
|
+
/** Receipt from the response; body is fully read. Returns unknown cost when evidence is absent. */
|
|
35
|
+
extract(operation: ProviderOperation, request: ProviderRequest, response: ProviderResponse, body: unknown): ReceiptDraft;
|
|
36
|
+
/** Provider request id for replay and reconciliation, when present. */
|
|
37
|
+
requestId(body: unknown): string | undefined;
|
|
38
|
+
/** Parse the balance probe body; without it the catalog reads the declared JSON pointers. */
|
|
39
|
+
balance?(body: unknown): BalanceSnapshot;
|
|
40
|
+
/** Parse a price-list endpoint body into PriceRow[] (for priceList.kind endpoint). */
|
|
41
|
+
priceList?(body: unknown, checkedAt: string): PriceRow[];
|
|
42
|
+
/** Parse a billing export into matchable lines. */
|
|
43
|
+
billingExport?(input: unknown): BillingLine[];
|
|
44
|
+
};
|
|
45
|
+
export type ProviderModule = {
|
|
46
|
+
descriptor: ProviderDescriptor;
|
|
47
|
+
extractors: Extractors;
|
|
48
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usagekit/providers",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"description": "Provider descriptors, catalog, price tables, extractors, fixtures and probes.",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./testing": {
|
|
13
|
+
"types": "./dist/testing/index.d.ts",
|
|
14
|
+
"import": "./dist/testing/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./eslint": {
|
|
17
|
+
"types": "./dist/eslint.d.ts",
|
|
18
|
+
"import": "./dist/eslint.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"!dist/**/*.tsbuildinfo",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"NOTICE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -b",
|
|
29
|
+
"typecheck": "tsc -b",
|
|
30
|
+
"prepublishOnly": "node ../../scripts/publish-guard.mjs",
|
|
31
|
+
"prepack": "node ../../scripts/package-license.mjs"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@usagekit/core": "0.4.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vitest": ">=5.0.1 <6",
|
|
38
|
+
"eslint": ">=9 <11"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"vitest": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"eslint": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"sideEffects": false,
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "restricted",
|
|
51
|
+
"registry": "https://registry.npmjs.org/"
|
|
52
|
+
}
|
|
53
|
+
}
|