@absolutejs/billing 0.6.0 → 0.8.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/CHANGELOG.md ADDED
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@absolutejs/billing`.
4
+
5
+ This file is generated by `absolute-changelog` from the entries in
6
+ `changelog/`. Edit an entry, not this file — and add new ones under
7
+ `changelog/unreleased/`.
8
+
9
+ ## 0.8.0 — 2026-09-11
10
+
11
+ ### Added
12
+
13
+ - **Add single-use purchase-only checkout handoffs with hashed capabilities, account binding and CSRF verification**
14
+
15
+ ## 0.7.0 — 2026-09-11
16
+
17
+ ### Added
18
+
19
+ - **Add durable prepaid credit accounts, reservations, and capped idempotent work settlement**
20
+
21
+ ---
22
+
23
+ ## Earlier releases
24
+
25
+ # @absolutejs/billing changelog
26
+
27
+ ## 0.1.0 — 2026-05-31
28
+
29
+ Initial release. Closes G13 from the second-pass PaaS audit — the
30
+ substrate now has a pure cost-model layer between
31
+ `@absolutejs/metering` and any invoicing backend.
32
+
33
+ ### Added
34
+
35
+ - **`createPlan({ name, basePriceMicros?, pricedDimensions,
36
+ currency?, rounding?, minimumChargeMicros?, metadata? })`** —
37
+ declarative pricing config. Validates tiered dimensions at
38
+ construction: non-empty, final tier must be `upTo: Infinity`,
39
+ bounds must be monotonically non-decreasing.
40
+ - **`computeInvoice({ plan, period, tenant, usage, currency? })`**
41
+ — pure function. Returns `{ tenant, plan, currency, period,
42
+ lineItems, totalMicros, totalUnits, metadata? }`.
43
+ - **Three pricing shapes** per dimension: flat `perUnitMicros`,
44
+ graduated `tiers`, or custom `price(quantity) => micros`.
45
+ - **`freeTier`** per dimension subtracted before pricing.
46
+ - **`unit`** divisor so `bytesEgress` priced as MB ↔ `unit:
47
+ 1024*1024`.
48
+ - **Rounding modes**: `'truncate'` (default — sub-cent → $0.00 to
49
+ match operator intuition) and `'round-half-up'`.
50
+ - **`minimumChargeMicros`** floor with an explicit
51
+ `'minimum-charge-adjustment'` line item that captures the gap
52
+ (transparent, not magical).
53
+ - **`formatMicros(amount, currency, { minorUnits? })`** — pure
54
+ string formatter, honors zero-minor-unit currencies (JPY etc.).
55
+ - **All money in integer micros** (1/1,000,000 of a unit) — same
56
+ denomination Stripe uses internally. Float drift structurally
57
+ impossible.
58
+
59
+ ### Design notes
60
+
61
+ - Pure functions throughout — no IO, no SDK peers, no side effects.
62
+ The control plane can preview invoices, re-price past periods
63
+ under a proposed plan, and dry-run plan changes without touching
64
+ any external system.
65
+ - Invoice push (Stripe, QuickBooks, mailed PDF) lives OUTSIDE this
66
+ package in `@absolutejs/billing-adapters/*`.
67
+ - Substrate is intentionally policy-free: it doesn't pick the plan
68
+ for a tenant, doesn't fetch usage, doesn't store invoices. Those
69
+ are control-plane concerns.
70
+
71
+ ### Tests
72
+
73
+ 34 covering: tier validation; flat per-unit; zero / missing /
74
+ negative / NaN usage; free tier (below / above / at boundary);
75
+ unit divisor + truncation + round-half-up; tiered (single band /
76
+ multi-band / unbounded final); tiered + free tier composition;
77
+ custom price fn; chargedQuantity passed to custom fn; base fee
78
+ emission + suppression-when-zero; minimum-charge top-up + no-op
79
+ when above floor; currency defaults + override; metadata
80
+ flow-through; realistic full-Usage shape; label override;
81
+ formatMicros (whole / sub-cent / negative / zero-minor-unit).
82
+
83
+ ### License
84
+
85
+ BSL-1.1 with named carveout against hosted SaaS-billing platforms
86
+ (Metronome, Orb, Lago, Stripe Billing, m3ter, Chargebee). Change
87
+ date: 2030-05-31 (Apache 2.0).
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@absolutejs/billing`
2
2
 
3
- > Cost-model substrate for the AbsoluteJS PaaS.
3
+ > Provider-neutral pricing and invoice computation used by the hosted AbsoluteJS.ai platform.
4
4
 
5
5
  `@absolutejs/billing` is the pure-function layer between
6
6
  `@absolutejs/metering` (which collects usage events) and an
@@ -94,3 +94,11 @@ mailed-PDF generator) lives outside this package, in
94
94
  BSL-1.1 with named carveout against hosted SaaS billing platforms
95
95
  (Metronome, Orb, Lago, Stripe Billing, m3ter, Chargebee). See
96
96
  `LICENSE`. Change date: **2030-05-31** → Apache 2.0.
97
+
98
+ ## Prepaid service credits
99
+
100
+ [Durable balances, reservations, capped work, and migration rules](docs/prepaid-credits.md) are available through the `prepaid`, `prepaid-postgres`, and `credit-work` subpaths.
101
+
102
+ ## Secure credit checkout
103
+
104
+ [Purchase-only handoffs](docs/checkout-handoffs.md) provide expiring, single-use browser capabilities without a full login session.
package/changelog.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "contract": 1,
3
+ "name": "@absolutejs/billing",
4
+ "releases": [
5
+ {
6
+ "version": "0.8.0",
7
+ "date": "2026-09-11",
8
+ "changes": [
9
+ {
10
+ "kind": "added",
11
+ "summary": "Add single-use purchase-only checkout handoffs with hashed capabilities, account binding and CSRF verification"
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "changes": [
17
+ {
18
+ "kind": "added",
19
+ "summary": "Add durable prepaid credit accounts, reservations, and capped idempotent work settlement"
20
+ }
21
+ ],
22
+ "date": "2026-09-11",
23
+ "version": "0.7.0"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,39 @@
1
+ import type { CreditSqlClient } from "./prepaidPostgres";
2
+ export type CheckoutQuote = {
3
+ productId: string;
4
+ amountCents: number;
5
+ currency: string;
6
+ credits: number;
7
+ };
8
+ export type CheckoutHandoff = {
9
+ id: string;
10
+ accountId: string;
11
+ quote: CheckoutQuote;
12
+ expiresAt: number;
13
+ };
14
+ /** Apply explicitly during deployment. Secrets are stored only as SHA-256 hashes. */
15
+ export declare const checkoutHandoffPostgresSchemaSql: () => string;
16
+ /** This is a purchase-only capability, never a login or a saved-card authorization.
17
+ * The application binds the authenticated account and prices the quote. */
18
+ export declare const createPostgresCheckoutHandoffs: (db: CreditSqlClient, now?: () => number) => {
19
+ issue(accountId: string, quote: CheckoutQuote): Promise<{
20
+ id: `${string}-${string}-${string}-${string}-${string}`;
21
+ code: string;
22
+ expiresAt: number;
23
+ }>;
24
+ /** Invoke only on a deliberate same-origin POST, never a preview GET.
25
+ * Atomic exchange makes concurrent redemption and link replay fail closed. */
26
+ exchange(code: string, browserAccountId?: string | null): Promise<{
27
+ handoff: CheckoutHandoff;
28
+ session: string;
29
+ csrf: string;
30
+ expiresAt: number;
31
+ } | null>;
32
+ authorize(session: string, csrf: string, browserAccountId?: string | null): Promise<CheckoutHandoff | null>;
33
+ /** Public IDs are not capabilities: status always requires account binding. */
34
+ get(accountId: string, id: string): Promise<CheckoutHandoff | null>;
35
+ };
36
+ /** Put the code in a fragment so previews, servers and referrers don't receive it.
37
+ * The landing page must remove it before loading any third-party code. */
38
+ export declare const checkoutHandoffUrl: (landingUrl: string, code: string) => string;
39
+ export declare const checkoutSameOrigin: (request: Request, origin: string) => boolean;
@@ -0,0 +1,108 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/checkoutHandoff.ts
18
+ import { createHash, randomBytes, randomUUID } from "crypto";
19
+ var lifetimeMs = 15 * 60 * 1000;
20
+ var hash = (secret) => createHash("sha256").update(secret).digest("hex");
21
+ var secret = () => randomBytes(32).toString("base64url");
22
+ var validSecret = (value) => /^[A-Za-z0-9_-]{43}$/.test(value);
23
+ var validQuote = (value) => typeof value.productId === "string" && value.productId.length > 0 && value.productId.length <= 128 && Number.isSafeInteger(value.amountCents) && value.amountCents > 0 && /^[A-Z]{3}$/.test(value.currency) && Number.isSafeInteger(value.credits) && value.credits > 0;
24
+ var checkoutHandoffPostgresSchemaSql = () => `
25
+ CREATE SCHEMA IF NOT EXISTS billing_checkout;
26
+ CREATE TABLE IF NOT EXISTS billing_checkout.handoffs (
27
+ id text PRIMARY KEY, account_id text NOT NULL, quote jsonb NOT NULL,
28
+ code_hash text NOT NULL UNIQUE, expires_at bigint NOT NULL,
29
+ session_hash text UNIQUE, csrf_hash text, session_expires_at bigint
30
+ );
31
+ CREATE INDEX IF NOT EXISTS handoffs_account ON billing_checkout.handoffs(account_id, id);
32
+ `;
33
+ var decode = (row) => {
34
+ if (!row)
35
+ return null;
36
+ const quote = row.quote;
37
+ if (!quote || typeof quote !== "object" || !("productId" in quote) || !("amountCents" in quote) || !("currency" in quote) || !("credits" in quote) || typeof quote.productId !== "string" || typeof quote.amountCents !== "number" || typeof quote.currency !== "string" || typeof quote.credits !== "number" || typeof row.id !== "string" || typeof row.account_id !== "string")
38
+ throw new Error("Invalid checkout record");
39
+ const parsed = {
40
+ productId: quote.productId,
41
+ amountCents: quote.amountCents,
42
+ currency: quote.currency,
43
+ credits: quote.credits
44
+ };
45
+ if (!validQuote(parsed))
46
+ throw new Error("Invalid checkout quote");
47
+ return {
48
+ id: row.id,
49
+ accountId: row.account_id,
50
+ quote: parsed,
51
+ expiresAt: Number(row.expires_at)
52
+ };
53
+ };
54
+ var createPostgresCheckoutHandoffs = (db, now = Date.now) => ({
55
+ async issue(accountId, quote) {
56
+ if (!accountId || !validQuote(quote))
57
+ throw new Error("Invalid checkout request");
58
+ const code = secret();
59
+ const id = randomUUID();
60
+ const expiresAt = now() + lifetimeMs;
61
+ await db.query(`INSERT INTO billing_checkout.handoffs(id,account_id,quote,code_hash,expires_at) VALUES ($1,$2,$3::jsonb,$4,$5)`, [id, accountId, JSON.stringify(quote), hash(code), expiresAt]);
62
+ return { id, code, expiresAt };
63
+ },
64
+ async exchange(code, browserAccountId = null) {
65
+ if (!validSecret(code))
66
+ return null;
67
+ const session = secret();
68
+ const csrf = secret();
69
+ const expiresAt = now() + lifetimeMs;
70
+ const { rows } = await db.query(`UPDATE billing_checkout.handoffs SET session_hash=$2,csrf_hash=$3,session_expires_at=$4 WHERE code_hash=$1 AND session_hash IS NULL AND expires_at>$5 AND ($6::text IS NULL OR account_id=$6) RETURNING *`, [
71
+ hash(code),
72
+ hash(session),
73
+ hash(csrf),
74
+ expiresAt,
75
+ now(),
76
+ browserAccountId
77
+ ]);
78
+ const handoff = decode(rows[0]);
79
+ return handoff ? { handoff, session, csrf, expiresAt } : null;
80
+ },
81
+ async authorize(session, csrf, browserAccountId = null) {
82
+ if (!validSecret(session) || !validSecret(csrf))
83
+ return null;
84
+ const { rows } = await db.query(`SELECT * FROM billing_checkout.handoffs WHERE session_hash=$1 AND csrf_hash=$2 AND session_expires_at>$3 AND ($4::text IS NULL OR account_id=$4)`, [hash(session), hash(csrf), now(), browserAccountId]);
85
+ return decode(rows[0]);
86
+ },
87
+ async get(accountId, id) {
88
+ const { rows } = await db.query(`SELECT * FROM billing_checkout.handoffs WHERE id=$1 AND account_id=$2`, [id, accountId]);
89
+ return decode(rows[0]);
90
+ }
91
+ });
92
+ var checkoutHandoffUrl = (landingUrl, code) => {
93
+ const url = new URL(landingUrl);
94
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || !validSecret(code))
95
+ throw new Error("Invalid checkout landing URL");
96
+ url.hash = code;
97
+ return url.href;
98
+ };
99
+ var checkoutSameOrigin = (request, origin) => request.method === "POST" && request.headers.get("origin") === new URL(origin).origin && [null, "same-origin"].includes(request.headers.get("sec-fetch-site"));
100
+ export {
101
+ checkoutHandoffPostgresSchemaSql,
102
+ checkoutHandoffUrl,
103
+ checkoutSameOrigin,
104
+ createPostgresCheckoutHandoffs
105
+ };
106
+
107
+ //# debugId=F96D1D9F1E1CA67A64756E2164756E21
108
+ //# sourceMappingURL=checkoutHandoff.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/checkoutHandoff.ts"],
4
+ "sourcesContent": [
5
+ "import { createHash, randomBytes, randomUUID } from \"node:crypto\";\nimport type { CreditSqlClient } from \"./prepaidPostgres\";\n\nexport type CheckoutQuote = {\n productId: string;\n amountCents: number;\n currency: string;\n credits: number;\n};\nexport type CheckoutHandoff = {\n id: string;\n accountId: string;\n quote: CheckoutQuote;\n expiresAt: number;\n};\nconst lifetimeMs = 15 * 60 * 1000;\nconst hash = (secret: string) =>\n createHash(\"sha256\").update(secret).digest(\"hex\");\nconst secret = () => randomBytes(32).toString(\"base64url\");\nconst validSecret = (value: string) => /^[A-Za-z0-9_-]{43}$/.test(value);\nconst validQuote = (value: CheckoutQuote) =>\n typeof value.productId === \"string\" &&\n value.productId.length > 0 &&\n value.productId.length <= 128 &&\n Number.isSafeInteger(value.amountCents) &&\n value.amountCents > 0 &&\n /^[A-Z]{3}$/.test(value.currency) &&\n Number.isSafeInteger(value.credits) &&\n value.credits > 0;\n\n/** Apply explicitly during deployment. Secrets are stored only as SHA-256 hashes. */\nexport const checkoutHandoffPostgresSchemaSql = () => `\nCREATE SCHEMA IF NOT EXISTS billing_checkout;\nCREATE TABLE IF NOT EXISTS billing_checkout.handoffs (\n id text PRIMARY KEY, account_id text NOT NULL, quote jsonb NOT NULL,\n code_hash text NOT NULL UNIQUE, expires_at bigint NOT NULL,\n session_hash text UNIQUE, csrf_hash text, session_expires_at bigint\n);\nCREATE INDEX IF NOT EXISTS handoffs_account ON billing_checkout.handoffs(account_id, id);\n`;\nconst decode = (\n row: Record<string, unknown> | undefined,\n): CheckoutHandoff | null => {\n if (!row) return null;\n const quote = row.quote;\n if (\n !quote ||\n typeof quote !== \"object\" ||\n !(\"productId\" in quote) ||\n !(\"amountCents\" in quote) ||\n !(\"currency\" in quote) ||\n !(\"credits\" in quote) ||\n typeof quote.productId !== \"string\" ||\n typeof quote.amountCents !== \"number\" ||\n typeof quote.currency !== \"string\" ||\n typeof quote.credits !== \"number\" ||\n typeof row.id !== \"string\" ||\n typeof row.account_id !== \"string\"\n )\n throw new Error(\"Invalid checkout record\");\n const parsed = {\n productId: quote.productId,\n amountCents: quote.amountCents,\n currency: quote.currency,\n credits: quote.credits,\n };\n if (!validQuote(parsed)) throw new Error(\"Invalid checkout quote\");\n return {\n id: row.id,\n accountId: row.account_id,\n quote: parsed,\n expiresAt: Number(row.expires_at),\n };\n};\n/** This is a purchase-only capability, never a login or a saved-card authorization.\n * The application binds the authenticated account and prices the quote. */\nexport const createPostgresCheckoutHandoffs = (\n db: CreditSqlClient,\n now = Date.now,\n) => ({\n async issue(accountId: string, quote: CheckoutQuote) {\n if (!accountId || !validQuote(quote))\n throw new Error(\"Invalid checkout request\");\n const code = secret();\n const id = randomUUID();\n const expiresAt = now() + lifetimeMs;\n await db.query(\n `INSERT INTO billing_checkout.handoffs(id,account_id,quote,code_hash,expires_at) VALUES ($1,$2,$3::jsonb,$4,$5)`,\n [id, accountId, JSON.stringify(quote), hash(code), expiresAt],\n );\n return { id, code, expiresAt };\n },\n /** Invoke only on a deliberate same-origin POST, never a preview GET.\n * Atomic exchange makes concurrent redemption and link replay fail closed. */\n async exchange(code: string, browserAccountId: string | null = null) {\n if (!validSecret(code)) return null;\n const session = secret();\n const csrf = secret();\n const expiresAt = now() + lifetimeMs;\n const { rows } = await db.query(\n `UPDATE billing_checkout.handoffs SET session_hash=$2,csrf_hash=$3,session_expires_at=$4 WHERE code_hash=$1 AND session_hash IS NULL AND expires_at>$5 AND ($6::text IS NULL OR account_id=$6) RETURNING *`,\n [\n hash(code),\n hash(session),\n hash(csrf),\n expiresAt,\n now(),\n browserAccountId,\n ],\n );\n const handoff = decode(rows[0]);\n return handoff ? { handoff, session, csrf, expiresAt } : null;\n },\n async authorize(\n session: string,\n csrf: string,\n browserAccountId: string | null = null,\n ) {\n if (!validSecret(session) || !validSecret(csrf)) return null;\n const { rows } = await db.query(\n `SELECT * FROM billing_checkout.handoffs WHERE session_hash=$1 AND csrf_hash=$2 AND session_expires_at>$3 AND ($4::text IS NULL OR account_id=$4)`,\n [hash(session), hash(csrf), now(), browserAccountId],\n );\n return decode(rows[0]);\n },\n /** Public IDs are not capabilities: status always requires account binding. */\n async get(accountId: string, id: string) {\n const { rows } = await db.query(\n `SELECT * FROM billing_checkout.handoffs WHERE id=$1 AND account_id=$2`,\n [id, accountId],\n );\n return decode(rows[0]);\n },\n});\n\n/** Put the code in a fragment so previews, servers and referrers don't receive it.\n * The landing page must remove it before loading any third-party code. */\nexport const checkoutHandoffUrl = (landingUrl: string, code: string) => {\n const url = new URL(landingUrl);\n if (\n url.protocol !== \"https:\" ||\n url.username ||\n url.password ||\n url.search ||\n url.hash ||\n !validSecret(code)\n )\n throw new Error(\"Invalid checkout landing URL\");\n url.hash = code;\n return url.href;\n};\nexport const checkoutSameOrigin = (request: Request, origin: string) =>\n request.method === \"POST\" &&\n request.headers.get(\"origin\") === new URL(origin).origin &&\n [null, \"same-origin\"].includes(request.headers.get(\"sec-fetch-site\"));\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "F96D1D9F1E1CA67A64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,25 @@
1
+ import { type CreditSqlClient } from "./prepaidPostgres";
2
+ export type CreditWork = {
3
+ request: string;
4
+ budget: number;
5
+ charged: number;
6
+ absorbed: number;
7
+ status: "running" | "completed" | "failed";
8
+ result: string | null;
9
+ };
10
+ export declare const creditWorkPostgresSchemaSql: (schema?: string) => string;
11
+ /** Durable execution claim + capped customer charge. A crash leaves work running
12
+ * and its credits held: never automatically rerun an uncertain external effect.
13
+ * Provider cost over the agreed budget is recorded as absorbed, not customer debt. */
14
+ export declare const createPostgresCreditWork: (client: CreditSqlClient, schema?: string) => {
15
+ get: (accountId: string, workId: string) => Promise<CreditWork | null>;
16
+ begin: (accountId: string, workId: string, request: string, budget: number) => Promise<{
17
+ fresh: boolean;
18
+ work: CreditWork;
19
+ }>;
20
+ record: (accountId: string, workId: string, eventId: string, credits: number, reference: string) => Promise<{
21
+ charged: number;
22
+ fresh: boolean;
23
+ }>;
24
+ finish: (accountId: string, workId: string, result: string, failed?: boolean) => Promise<CreditWork>;
25
+ };