@usagekit/providers 0.4.0 → 0.6.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.
@@ -0,0 +1,12 @@
1
+ import type { BillingLine } from "@usagekit/core";
2
+ import type { ProviderModule } from "./types.js";
3
+ export type ParsedBillingExport = {
4
+ provider: string;
5
+ fileHash: string;
6
+ lines: readonly BillingLine[];
7
+ };
8
+ /** Parse the advertised export locally, hashing the original UTF-8 bytes before normalization.
9
+ * This performs no provider request and grants no accounting or dispatch authority. The host
10
+ * supplies its verified connection, window and attribution to Meter.importBilling separately.
11
+ */
12
+ export declare function parseBillingExport(provider: ProviderModule, original: string): Promise<ParsedBillingExport>;
@@ -0,0 +1,66 @@
1
+ /** Exact USD numeric lexeme at the Meter's six-digit money scale. Never rounds. */
2
+ function exactCostUnits(source) {
3
+ if (typeof source !== "string")
4
+ return null;
5
+ const match = /^(0|[1-9]\d*)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(source);
6
+ if (!match)
7
+ return null;
8
+ const fraction = match[2] ?? "", exponent = Number(match[3] ?? "0");
9
+ let coefficient = (match[1] + fraction).replace(/^0+/, "");
10
+ if (!coefficient)
11
+ return 0n;
12
+ if (!Number.isSafeInteger(exponent))
13
+ return null;
14
+ let scale = fraction.length - exponent;
15
+ const trimmed = coefficient.replace(/0+$/, "");
16
+ scale -= coefficient.length - trimmed.length;
17
+ coefficient = trimmed;
18
+ if (scale > 6 || scale < -324)
19
+ return null;
20
+ if (coefficient.length + 6 - scale > 19)
21
+ return null;
22
+ const units = BigInt(coefficient) * 10n ** BigInt(6 - scale);
23
+ return units <= 2n ** 63n - 1n ? units : null;
24
+ }
25
+ /** Parse the advertised export locally, hashing the original UTF-8 bytes before normalization.
26
+ * This performs no provider request and grants no accounting or dispatch authority. The host
27
+ * supplies its verified connection, window and attribution to Meter.importBilling separately.
28
+ */
29
+ export async function parseBillingExport(provider, original) {
30
+ const { billingExport, id } = provider.descriptor;
31
+ if (billingExport.kind === "none" || !provider.extractors.billingExport)
32
+ throw new Error(`Billing export is unavailable for ${id}`);
33
+ const bytes = new TextEncoder().encode(original);
34
+ const input = billingExport.kind === "task-history" ? JSON.parse(original) : original;
35
+ let lines = provider.extractors.billingExport(input);
36
+ // The current task-history format has one row per task. Never silently import a partial
37
+ // export when the descriptor parser cannot read a charge, identity or occurrence time.
38
+ if (billingExport.kind === "task-history") {
39
+ const tasks = input && typeof input === "object" && !Array.isArray(input)
40
+ ? input.tasks
41
+ : undefined;
42
+ if (!Array.isArray(tasks) || lines.length !== tasks.length)
43
+ throw new Error(`Invalid task-history billing export for ${id}`);
44
+ // JSON.parse's source context retains the original numeric spelling even when a number
45
+ // cannot survive binary floating point. Runtimes without it fail closed for charged rows.
46
+ const lexical = JSON.parse(original, (_key, value, context) => typeof value === "number" ? context?.source : value);
47
+ const exactCosts = new Map();
48
+ for (const task of lexical.tasks) {
49
+ const exact = exactCostUnits(task.cost);
50
+ if (exact === null || typeof task.id !== "string" || !task.id || exactCosts.has(task.id))
51
+ throw new Error(`Invalid task-history billing cost for ${id}`);
52
+ exactCosts.set(task.id, exact);
53
+ }
54
+ const matched = new Set();
55
+ lines = lines.map((line) => {
56
+ const requestId = line.providerRequestId;
57
+ if (!requestId || !exactCosts.has(requestId) || matched.has(requestId))
58
+ throw new Error(`Invalid task-history billing identity for ${id}`);
59
+ matched.add(requestId);
60
+ return { ...line, cost: { units: exactCosts.get(requestId), currency: "USD" } };
61
+ });
62
+ }
63
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
64
+ const fileHash = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
65
+ return { provider: id, fileHash, lines };
66
+ }
package/dist/index.d.ts CHANGED
@@ -17,3 +17,5 @@ export { dataforseo } from "./dataforseo/index.js";
17
17
  export { serpapi } from "./serpapi/index.js";
18
18
  export { executeProviderRequest } from "./execute.js";
19
19
  export type { ProviderExecution, ProviderExecutionInput } from "./execute.js";
20
+ export { parseBillingExport } from "./billing.js";
21
+ export type { ParsedBillingExport } from "./billing.js";
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export { loadFixtures, missingFixtures, verifyFixture } from "./fixtures.js";
8
8
  export { dataforseo } from "./dataforseo/index.js";
9
9
  export { serpapi } from "./serpapi/index.js";
10
10
  export { executeProviderRequest } from "./execute.js";
11
+ export { parseBillingExport } from "./billing.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagekit/providers",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Provider descriptors, catalog, price tables, extractors, fixtures and probes.",
@@ -31,7 +31,7 @@
31
31
  "prepack": "node ../../scripts/package-license.mjs"
32
32
  },
33
33
  "dependencies": {
34
- "@usagekit/core": "0.4.0"
34
+ "@usagekit/core": "0.6.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "vitest": ">=5.0.1 <6",
@@ -49,5 +49,14 @@
49
49
  "publishConfig": {
50
50
  "access": "restricted",
51
51
  "registry": "https://registry.npmjs.org/"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/msniezynski/usagekit.git",
56
+ "directory": "packages/providers"
57
+ },
58
+ "homepage": "https://usagekit.dev",
59
+ "bugs": {
60
+ "url": "https://github.com/msniezynski/usagekit/issues"
52
61
  }
53
62
  }