@absolutejs/billing 0.7.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 CHANGED
@@ -6,6 +6,12 @@ This file is generated by `absolute-changelog` from the entries in
6
6
  `changelog/`. Edit an entry, not this file — and add new ones under
7
7
  `changelog/unreleased/`.
8
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
+
9
15
  ## 0.7.0 — 2026-09-11
10
16
 
11
17
  ### Added
package/README.md CHANGED
@@ -98,3 +98,7 @@ BSL-1.1 with named carveout against hosted SaaS billing platforms
98
98
  ## Prepaid service credits
99
99
 
100
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 CHANGED
@@ -1,16 +1,26 @@
1
1
  {
2
- "contract": 1,
3
- "name": "@absolutejs/billing",
4
- "releases": [
5
- {
6
- "changes": [
7
- {
8
- "kind": "added",
9
- "summary": "Add durable prepaid credit accounts, reservations, and capped idempotent work settlement"
10
- }
11
- ],
12
- "date": "2026-09-11",
13
- "version": "0.7.0"
14
- }
15
- ]
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
+ ]
16
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": ";;;;;;;;;;;;;;;;;AAAA;AAeA,IAAM,aAAa,KAAK,KAAK;AAC7B,IAAM,OAAO,CAAC,WACZ,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAClD,IAAM,SAAS,MAAM,YAAY,EAAE,EAAE,SAAS,WAAW;AACzD,IAAM,cAAc,CAAC,UAAkB,sBAAsB,KAAK,KAAK;AACvE,IAAM,aAAa,CAAC,UAClB,OAAO,MAAM,cAAc,YAC3B,MAAM,UAAU,SAAS,KACzB,MAAM,UAAU,UAAU,OAC1B,OAAO,cAAc,MAAM,WAAW,KACtC,MAAM,cAAc,KACpB,aAAa,KAAK,MAAM,QAAQ,KAChC,OAAO,cAAc,MAAM,OAAO,KAClC,MAAM,UAAU;AAGX,IAAM,mCAAmC,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAStD,IAAM,SAAS,CACb,QAC2B;AAAA,EAC3B,IAAI,CAAC;AAAA,IAAK,OAAO;AAAA,EACjB,MAAM,QAAQ,IAAI;AAAA,EAClB,IACE,CAAC,SACD,OAAO,UAAU,YACjB,EAAE,eAAe,UACjB,EAAE,iBAAiB,UACnB,EAAE,cAAc,UAChB,EAAE,aAAa,UACf,OAAO,MAAM,cAAc,YAC3B,OAAO,MAAM,gBAAgB,YAC7B,OAAO,MAAM,aAAa,YAC1B,OAAO,MAAM,YAAY,YACzB,OAAO,IAAI,OAAO,YAClB,OAAO,IAAI,eAAe;AAAA,IAE1B,MAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C,MAAM,SAAS;AAAA,IACb,WAAW,MAAM;AAAA,IACjB,aAAa,MAAM;AAAA,IACnB,UAAU,MAAM;AAAA,IAChB,SAAS,MAAM;AAAA,EACjB;AAAA,EACA,IAAI,CAAC,WAAW,MAAM;AAAA,IAAG,MAAM,IAAI,MAAM,wBAAwB;AAAA,EACjE,OAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,WAAW,IAAI;AAAA,IACf,OAAO;AAAA,IACP,WAAW,OAAO,IAAI,UAAU;AAAA,EAClC;AAAA;AAIK,IAAM,iCAAiC,CAC5C,IACA,MAAM,KAAK,SACP;AAAA,OACE,MAAK,CAAC,WAAmB,OAAsB;AAAA,IACnD,IAAI,CAAC,aAAa,CAAC,WAAW,KAAK;AAAA,MACjC,MAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C,MAAM,OAAO,OAAO;AAAA,IACpB,MAAM,KAAK,WAAW;AAAA,IACtB,MAAM,YAAY,IAAI,IAAI;AAAA,IAC1B,MAAM,GAAG,MACP,kHACA,CAAC,IAAI,WAAW,KAAK,UAAU,KAAK,GAAG,KAAK,IAAI,GAAG,SAAS,CAC9D;AAAA,IACA,OAAO,EAAE,IAAI,MAAM,UAAU;AAAA;AAAA,OAIzB,SAAQ,CAAC,MAAc,mBAAkC,MAAM;AAAA,IACnE,IAAI,CAAC,YAAY,IAAI;AAAA,MAAG,OAAO;AAAA,IAC/B,MAAM,UAAU,OAAO;AAAA,IACvB,MAAM,OAAO,OAAO;AAAA,IACpB,MAAM,YAAY,IAAI,IAAI;AAAA,IAC1B,QAAQ,SAAS,MAAM,GAAG,MACxB,6MACA;AAAA,MACE,KAAK,IAAI;AAAA,MACT,KAAK,OAAO;AAAA,MACZ,KAAK,IAAI;AAAA,MACT;AAAA,MACA,IAAI;AAAA,MACJ;AAAA,IACF,CACF;AAAA,IACA,MAAM,UAAU,OAAO,KAAK,EAAE;AAAA,IAC9B,OAAO,UAAU,EAAE,SAAS,SAAS,MAAM,UAAU,IAAI;AAAA;AAAA,OAErD,UAAS,CACb,SACA,MACA,mBAAkC,MAClC;AAAA,IACA,IAAI,CAAC,YAAY,OAAO,KAAK,CAAC,YAAY,IAAI;AAAA,MAAG,OAAO;AAAA,IACxD,QAAQ,SAAS,MAAM,GAAG,MACxB,oJACA,CAAC,KAAK,OAAO,GAAG,KAAK,IAAI,GAAG,IAAI,GAAG,gBAAgB,CACrD;AAAA,IACA,OAAO,OAAO,KAAK,EAAE;AAAA;AAAA,OAGjB,IAAG,CAAC,WAAmB,IAAY;AAAA,IACvC,QAAQ,SAAS,MAAM,GAAG,MACxB,yEACA,CAAC,IAAI,SAAS,CAChB;AAAA,IACA,OAAO,OAAO,KAAK,EAAE;AAAA;AAEzB;AAIO,IAAM,qBAAqB,CAAC,YAAoB,SAAiB;AAAA,EACtE,MAAM,MAAM,IAAI,IAAI,UAAU;AAAA,EAC9B,IACE,IAAI,aAAa,YACjB,IAAI,YACJ,IAAI,YACJ,IAAI,UACJ,IAAI,QACJ,CAAC,YAAY,IAAI;AAAA,IAEjB,MAAM,IAAI,MAAM,8BAA8B;AAAA,EAChD,IAAI,OAAO;AAAA,EACX,OAAO,IAAI;AAAA;AAEN,IAAM,qBAAqB,CAAC,SAAkB,WACnD,QAAQ,WAAW,UACnB,QAAQ,QAAQ,IAAI,QAAQ,MAAM,IAAI,IAAI,MAAM,EAAE,UAClD,CAAC,MAAM,aAAa,EAAE,SAAS,QAAQ,QAAQ,IAAI,gBAAgB,CAAC;",
8
+ "debugId": "F96D1D9F1E1CA67A64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,9 @@
1
+ # Purchase-only checkout handoffs
2
+
3
+ `@absolutejs/billing/checkout-handoff` issues opaque 256-bit codes from a server-priced quote and authenticated account. Apply `checkoutHandoffPostgresSchemaSql()` explicitly; use `createPostgresCheckoutHandoffs` with the same parameterized SQL adapter as the prepaid ledger. This API is not login, consent to charge, or saved-payment-method access.
4
+
5
+ Deliver `checkoutHandoffUrl(httpsLandingUrl, code)` only on permitted host channels. The fragment prevents ordinary HTTP previews/referrers from receiving the code, but it is still a limited bearer capability visible in the assistant transcript. Never log it. Remove it synchronously before analytics or third-party scripts run. GET renders a neutral page; an explicit same-origin POST atomically exchanges the code. Reject a different signed-in browser account. Set the returned session in a Secure, HttpOnly, host-only SameSite=Strict cookie; retain CSRF only in the browser. Require `checkoutSameOrigin` AND `authorize(session, csrf, browserAccountId)` on every review/payment POST. Codes and sessions expire after 15 minutes; concurrent redemption succeeds once.
6
+
7
+ The limited session reveals only its fixed quote and purchase status. It cannot read profile, balance history, receipts, saved cards, or subscribe. Payment requires a new provider-hosted token and explicit user confirmation of the exact quote. Bind the gateway operation ID to the handoff ID; reuse the existing durable reserve/charge/recover/grant service. Never retry uncertain charges with new IDs. A status read is not a gateway operation. Account deletion/revocation remains the consumer's responsibility on issue and use. Retain financial records; expired unused handoffs may be removed by an operator's retention policy.
8
+
9
+ Host approval and supported UI are independent. MCP form elicitation must not collect payment credentials; use an approved external browser interaction. The handoff alone does not approve a commerce channel. Sources: https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation and the MCP package `docs/commerce-host-rules.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/billing",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Provider-neutral pricing and invoice computation used by the hosted AbsoluteJS.ai platform. Converts metered usage into exact integer-micro line items with tiers and allowances.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,6 +47,11 @@
47
47
  "types": "./dist/creditWork.d.ts",
48
48
  "import": "./dist/creditWork.js",
49
49
  "default": "./dist/creditWork.js"
50
+ },
51
+ "./checkout-handoff": {
52
+ "types": "./dist/checkoutHandoff.d.ts",
53
+ "import": "./dist/checkoutHandoff.js",
54
+ "default": "./dist/checkoutHandoff.js"
50
55
  }
51
56
  },
52
57
  "publishConfig": {
@@ -57,10 +62,11 @@
57
62
  "README.md",
58
63
  "changelog.json",
59
64
  "dist",
60
- "docs/prepaid-credits.md"
65
+ "docs/prepaid-credits.md",
66
+ "docs/checkout-handoffs.md"
61
67
  ],
62
68
  "scripts": {
63
- "build": "rm -rf dist && bun build src/index.ts src/ledger.ts src/prepaid.ts src/prepaidPostgres.ts src/creditWork.ts src/manifest.ts --root ./src --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json && absolute-manifest emit",
69
+ "build": "rm -rf dist && bun build src/index.ts src/ledger.ts src/prepaid.ts src/prepaidPostgres.ts src/creditWork.ts src/checkoutHandoff.ts src/manifest.ts --root ./src --outdir dist --sourcemap --target=bun && tsc --project tsconfig.build.json && absolute-manifest emit",
64
70
  "test": "bun test tests/",
65
71
  "typecheck": "tsc --noEmit",
66
72
  "format": "prettier --write \"./**/*.{ts,json,md}\"",