@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 +87 -0
- package/README.md +9 -1
- package/changelog.json +26 -0
- package/dist/checkoutHandoff.d.ts +39 -0
- package/dist/checkoutHandoff.js +108 -0
- package/dist/checkoutHandoff.js.map +10 -0
- package/dist/creditWork.d.ts +25 -0
- package/dist/creditWork.js +460 -0
- package/dist/creditWork.js.map +12 -0
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/ledger.js +6 -6
- package/dist/ledger.js.map +1 -1
- package/dist/manifest.js +211 -129
- package/dist/manifest.js.map +3 -3
- package/dist/prepaid.d.ts +101 -0
- package/dist/prepaid.js +271 -0
- package/dist/prepaid.js.map +10 -0
- package/dist/prepaidPostgres.d.ts +11 -0
- package/dist/prepaidPostgres.js +335 -0
- package/dist/prepaidPostgres.js.map +11 -0
- package/docs/checkout-handoffs.md +9 -0
- package/docs/prepaid-credits.md +29 -0
- package/package.json +35 -8
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
|
-
>
|
|
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
|
+
};
|