@krizaka/credits 0.1.3 → 0.1.4

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.
@@ -3,7 +3,7 @@ import {
3
3
  CREDITS_CODE,
4
4
  CreditsError,
5
5
  ON_EXHAUSTED
6
- } from "./chunk-WDHQ7QHY.js";
6
+ } from "./chunk-NKDQNDSD.js";
7
7
 
8
8
  // src/rules.ts
9
9
  import { isAfter } from "@krizaka/time";
@@ -20,7 +20,8 @@ function drawGranted(granted, need) {
20
20
  const dayLeft = s.grant.capPerDay === null ? Infinity : Math.max(0, s.grant.capPerDay - s.usedToday);
21
21
  const monthLeft = s.grant.capPerMonth === null ? Infinity : Math.max(0, s.grant.capPerMonth - s.usedThisMonth);
22
22
  const take = Math.min(left, s.remaining, dayLeft, monthLeft);
23
- if (take < Math.min(left, s.remaining)) capped ??= dayLeft <= monthLeft ? CREDITS_CODE.capDay : CREDITS_CODE.capMonth;
23
+ if (take < Math.min(left, s.remaining))
24
+ capped ??= dayLeft <= monthLeft ? CREDITS_CODE.capDay : CREDITS_CODE.capMonth;
24
25
  if (take > 0) {
25
26
  draws.push({ grantId: s.grant.id, amount: take });
26
27
  left -= take;
@@ -31,7 +32,9 @@ function drawGranted(granted, need) {
31
32
  function drawPurchased(purchased, need) {
32
33
  const draws = [];
33
34
  let left = need;
34
- const oldestFirst = [...purchased].sort((a, b) => a.grant.createdAt.valueOf() - b.grant.createdAt.valueOf() || a.grant.id.localeCompare(b.grant.id));
35
+ const oldestFirst = [...purchased].sort(
36
+ (a, b) => a.grant.createdAt.valueOf() - b.grant.createdAt.valueOf() || a.grant.id.localeCompare(b.grant.id)
37
+ );
35
38
  for (const s of oldestFirst) {
36
39
  if (left === 0) break;
37
40
  const take = Math.min(left, s.remaining);
@@ -47,9 +50,14 @@ function planDraws(states, need, now) {
47
50
  if (first.left === 0) return { ok: true, draws: first.draws };
48
51
  if (first.capped) return { ok: false, code: first.capped };
49
52
  const taken = new Map(first.draws.map((d) => [d.grantId, d.amount]));
50
- const stopped = live.some((s) => s.grant.bucket === BUCKET.granted && s.grant.onExhausted === ON_EXHAUSTED.stop && s.remaining - (taken.get(s.grant.id) ?? 0) <= 0);
53
+ const stopped = live.some(
54
+ (s) => s.grant.bucket === BUCKET.granted && s.grant.onExhausted === ON_EXHAUSTED.stop && s.remaining - (taken.get(s.grant.id) ?? 0) <= 0
55
+ );
51
56
  if (stopped) return { ok: false, code: CREDITS_CODE.exhausted };
52
- const rest = drawPurchased(live.filter((s) => s.grant.bucket === BUCKET.purchased && s.remaining > 0), first.left);
57
+ const rest = drawPurchased(
58
+ live.filter((s) => s.grant.bucket === BUCKET.purchased && s.remaining > 0),
59
+ first.left
60
+ );
53
61
  return rest.left === 0 ? { ok: true, draws: [...first.draws, ...rest.draws] } : { ok: false, code: CREDITS_CODE.paywall };
54
62
  }
55
63
  function skipFirst(draws, skip) {
@@ -63,12 +71,17 @@ function skipFirst(draws, skip) {
63
71
  return out;
64
72
  }
65
73
  function checkAmount(n, what = "an amount") {
66
- if (!Number.isSafeInteger(n) || n <= 0) throw new CreditsError(CREDITS_CODE.badAmount, `${what} of credits is a positive integer, got ${n}`);
74
+ if (!Number.isSafeInteger(n) || n <= 0)
75
+ throw new CreditsError(CREDITS_CODE.badAmount, `${what} of credits is a positive integer, got ${n}`);
67
76
  return n;
68
77
  }
69
78
  var KEY = /^[\x21-\x7e]{1,150}$/;
70
79
  function checkKey(key) {
71
- if (!KEY.test(key) || key.includes(":")) throw new CreditsError(CREDITS_CODE.badKey, "An idempotency key is 1 to 150 printable characters, without spaces or a colon");
80
+ if (!KEY.test(key) || key.includes(":"))
81
+ throw new CreditsError(
82
+ CREDITS_CODE.badKey,
83
+ "An idempotency key is 1 to 150 printable characters, without spaces or a colon"
84
+ );
72
85
  return key;
73
86
  }
74
87
  function checkSettings(s) {
@@ -78,21 +91,28 @@ function checkSettings(s) {
78
91
  throw new CreditsError(CREDITS_CODE.badSettings, `Not an IANA time zone: ${s.timeZone}`);
79
92
  }
80
93
  if (!Number.isSafeInteger(s.holdTtlSeconds) || s.holdTtlSeconds < HOLD_TTL_BOUNDS[0] || s.holdTtlSeconds > HOLD_TTL_BOUNDS[1]) {
81
- throw new CreditsError(CREDITS_CODE.badSettings, `holdTtlSeconds is ${HOLD_TTL_BOUNDS[0]} to ${HOLD_TTL_BOUNDS[1]}`);
94
+ throw new CreditsError(
95
+ CREDITS_CODE.badSettings,
96
+ `holdTtlSeconds is ${HOLD_TTL_BOUNDS[0]} to ${HOLD_TTL_BOUNDS[1]}`
97
+ );
82
98
  }
83
99
  return s;
84
100
  }
85
101
  var MAX_SOURCE = 200;
86
102
  function checkGrantedTerms(input, now) {
87
- if (!(input.expiresAt instanceof Date) || Number.isNaN(input.expiresAt.valueOf()) || input.expiresAt <= now) throw new CreditsError(CREDITS_CODE.badGrant, "A granted pot lapses in the future");
88
- for (const cap of [input.capPerDay, input.capPerMonth]) if (cap !== void 0 && cap !== null) checkAmount(cap, "a cap");
89
- if (input.onExhausted !== ON_EXHAUSTED.stop && input.onExhausted !== ON_EXHAUSTED.fallbackPaid) throw new CreditsError(CREDITS_CODE.badGrant, "onExhausted is stop or fallback_paid");
103
+ if (!(input.expiresAt instanceof Date) || Number.isNaN(input.expiresAt.valueOf()) || input.expiresAt <= now)
104
+ throw new CreditsError(CREDITS_CODE.badGrant, "A granted pot lapses in the future");
105
+ for (const cap of [input.capPerDay, input.capPerMonth])
106
+ if (cap !== void 0 && cap !== null) checkAmount(cap, "a cap");
107
+ if (input.onExhausted !== ON_EXHAUSTED.stop && input.onExhausted !== ON_EXHAUSTED.fallbackPaid)
108
+ throw new CreditsError(CREDITS_CODE.badGrant, "onExhausted is stop or fallback_paid");
90
109
  }
91
110
  function checkNewGrant(kind, input, now) {
92
111
  checkAmount(input.amount, "a grant");
93
112
  checkKey(input.idempotencyKey);
94
113
  if (!input.accountId) throw new CreditsError(CREDITS_CODE.badGrant, "A grant has an account");
95
- if (!input.source || input.source.length > MAX_SOURCE) throw new CreditsError(CREDITS_CODE.badGrant, `A source is 1 to ${MAX_SOURCE} characters`);
114
+ if (!input.source || input.source.length > MAX_SOURCE)
115
+ throw new CreditsError(CREDITS_CODE.badGrant, `A source is 1 to ${MAX_SOURCE} characters`);
96
116
  if (kind === "grant") checkGrantedTerms(input, now);
97
117
  }
98
118
 
@@ -106,4 +126,4 @@ export {
106
126
  checkSettings,
107
127
  checkNewGrant
108
128
  };
109
- //# sourceMappingURL=chunk-34Y2HXOB.js.map
129
+ //# sourceMappingURL=chunk-FPQSMNV2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules.ts"],"sourcesContent":["import { isAfter } from \"@krizaka/time\";\n\nimport {\n BUCKET,\n CREDITS_CODE,\n type CreditsCode,\n CreditsError,\n type CreditsSettings,\n type GrantState,\n ON_EXHAUSTED,\n} from \"./port\";\n\n/** How much of one grant a consumption takes. */\nexport interface Draw {\n grantId: string;\n amount: number;\n}\n\nexport type Plan = { ok: true; draws: Draw[] } | { ok: false; code: CreditsCode };\n\n/** The defaults and bounds of the settings (the admin changes them without a redeploy). */\nexport const CREDITS_DEFAULTS: CreditsSettings = { timeZone: \"America/Toronto\", holdTtlSeconds: 900 };\n// @fixed: bounds on the lifetime of a hold, in seconds: 30 seconds to a day (a sanity range, not a business parameter)\nexport const HOLD_TTL_BOUNDS = [30, 86_400] as const;\n\nconst alive = (s: GrantState, now: Date) =>\n !s.closed && (s.grant.expiresAt === null || isAfter(s.grant.expiresAt, now));\nconst byExpiry = (a: GrantState, b: GrantState) =>\n (a.grant.expiresAt?.valueOf() ?? Infinity) - (b.grant.expiresAt?.valueOf() ?? Infinity) ||\n a.grant.createdAt.valueOf() - b.grant.createdAt.valueOf() ||\n a.grant.id.localeCompare(b.grant.id);\n\n/** What the granted pots can give, each within its caps, and the first cap that held one back. */\nfunction drawGranted(\n granted: readonly GrantState[],\n need: number,\n): { draws: Draw[]; left: number; capped: CreditsCode | null } {\n const draws: Draw[] = [];\n let left = need;\n let capped: CreditsCode | null = null;\n for (const s of granted) {\n if (left === 0) break;\n const dayLeft = s.grant.capPerDay === null ? Infinity : Math.max(0, s.grant.capPerDay - s.usedToday);\n const monthLeft = s.grant.capPerMonth === null ? Infinity : Math.max(0, s.grant.capPerMonth - s.usedThisMonth);\n const take = Math.min(left, s.remaining, dayLeft, monthLeft);\n if (take < Math.min(left, s.remaining))\n capped ??= dayLeft <= monthLeft ? CREDITS_CODE.capDay : CREDITS_CODE.capMonth;\n if (take > 0) {\n draws.push({ grantId: s.grant.id, amount: take });\n left -= take;\n }\n }\n return { draws, left, capped };\n}\n\n/** What the purchased pots can give, the oldest first. */\nfunction drawPurchased(purchased: readonly GrantState[], need: number): { draws: Draw[]; left: number } {\n const draws: Draw[] = [];\n let left = need;\n const oldestFirst = [...purchased].sort(\n (a, b) => a.grant.createdAt.valueOf() - b.grant.createdAt.valueOf() || a.grant.id.localeCompare(b.grant.id),\n );\n for (const s of oldestFirst) {\n if (left === 0) break;\n const take = Math.min(left, s.remaining);\n draws.push({ grantId: s.grant.id, amount: take });\n left -= take;\n }\n return { draws, left };\n}\n\n/**\n * Plans where `need` credits come from, or says why they cannot. Pure: the same states give the same plan.\n *\n * Granted credits go first, the pot that lapses first first, each within its caps of the day and the month. If the granted pots cannot\n * give it all: a pot held back by a cap answers `CREDITS_CAP_DAY` / `CREDITS_CAP_MONTH` (the cap is a fair-use limit: purchased\n * credits do not bypass it); else a `stop` pot that is empty answers `CREDITS_EXHAUSTED`; else the purchased credits give the rest, and\n * when they cannot, `PAYWALL`. Nothing is partial: the plan covers the whole amount or refuses.\n * @param states every grant of the account with what is left (`CreditStore.grantStates`)\n * @param need credits wanted, a positive integer\n * @param now the injected clock's instant\n */\nexport function planDraws(states: readonly GrantState[], need: number, now: Date): Plan {\n const live = states.filter((s) => alive(s, now));\n const granted = live.filter((s) => s.grant.bucket === BUCKET.granted && s.remaining > 0).sort(byExpiry);\n const first = drawGranted(granted, need);\n if (first.left === 0) return { ok: true, draws: first.draws };\n if (first.capped) return { ok: false, code: first.capped };\n const taken = new Map(first.draws.map((d) => [d.grantId, d.amount]));\n const stopped = live.some(\n (s) =>\n s.grant.bucket === BUCKET.granted &&\n s.grant.onExhausted === ON_EXHAUSTED.stop &&\n s.remaining - (taken.get(s.grant.id) ?? 0) <= 0,\n );\n if (stopped) return { ok: false, code: CREDITS_CODE.exhausted };\n const rest = drawPurchased(\n live.filter((s) => s.grant.bucket === BUCKET.purchased && s.remaining > 0),\n first.left,\n );\n return rest.left === 0\n ? { ok: true, draws: [...first.draws, ...rest.draws] }\n : { ok: false, code: CREDITS_CODE.paywall };\n}\n\n/** Drops the first `skip` credits of a plan (the ones already held back by active holds), keeping the order. */\nexport function skipFirst(draws: readonly Draw[], skip: number): Draw[] {\n let toSkip = skip;\n const out: Draw[] = [];\n for (const d of draws) {\n const dropped = Math.min(toSkip, d.amount);\n toSkip -= dropped;\n if (d.amount > dropped) out.push({ grantId: d.grantId, amount: d.amount - dropped });\n }\n return out;\n}\n\n/** Whether `n` is an amount of credits: a positive safe integer. @throws CreditsError `CREDITS_BAD_AMOUNT` */\nexport function checkAmount(n: number, what = \"an amount\"): number {\n if (!Number.isSafeInteger(n) || n <= 0)\n throw new CreditsError(CREDITS_CODE.badAmount, `${what} of credits is a positive integer, got ${n}`);\n return n;\n}\n\nconst KEY = /^[\\x21-\\x7e]{1,150}$/;\n/** An idempotency key: printable ASCII without spaces, 1 to 150 characters. @throws CreditsError `CREDITS_BAD_KEY` */\nexport function checkKey(key: string): string {\n if (!KEY.test(key) || key.includes(\":\"))\n throw new CreditsError(\n CREDITS_CODE.badKey,\n \"An idempotency key is 1 to 150 printable characters, without spaces or a colon\",\n );\n return key;\n}\n\n/** Checks the settings the service was given. @throws CreditsError `CREDITS_BAD_SETTINGS` */\nexport function checkSettings(s: CreditsSettings): CreditsSettings {\n try {\n new Intl.DateTimeFormat(\"en\", { timeZone: s.timeZone });\n } catch {\n throw new CreditsError(CREDITS_CODE.badSettings, `Not an IANA time zone: ${s.timeZone}`);\n }\n if (\n !Number.isSafeInteger(s.holdTtlSeconds) ||\n s.holdTtlSeconds < HOLD_TTL_BOUNDS[0] ||\n s.holdTtlSeconds > HOLD_TTL_BOUNDS[1]\n ) {\n throw new CreditsError(\n CREDITS_CODE.badSettings,\n `holdTtlSeconds is ${HOLD_TTL_BOUNDS[0]} to ${HOLD_TTL_BOUNDS[1]}`,\n );\n }\n return s;\n}\n\n// @fixed: the longest source a line of the ledger carries (its CHECK)\nconst MAX_SOURCE = 200;\n\n/** The shape of what `grant` and `purchase` take, for the checks. */\ninterface NewGrantInput {\n accountId: string;\n amount: number;\n source: string;\n idempotencyKey: string;\n expiresAt?: Date;\n capPerDay?: number | null;\n capPerMonth?: number | null;\n onExhausted?: string;\n}\n\n/** The checks of a granted pot's own terms: it lapses in the future, its caps are whole numbers, it says what happens when it runs out. */\nfunction checkGrantedTerms(input: NewGrantInput, now: Date): void {\n if (!(input.expiresAt instanceof Date) || Number.isNaN(input.expiresAt.valueOf()) || input.expiresAt <= now)\n throw new CreditsError(CREDITS_CODE.badGrant, \"A granted pot lapses in the future\");\n for (const cap of [input.capPerDay, input.capPerMonth])\n if (cap !== undefined && cap !== null) checkAmount(cap, \"a cap\");\n if (input.onExhausted !== ON_EXHAUSTED.stop && input.onExhausted !== ON_EXHAUSTED.fallbackPaid)\n throw new CreditsError(CREDITS_CODE.badGrant, \"onExhausted is stop or fallback_paid\");\n}\n\n/**\n * Checks what `grant` or `purchase` was given, before any store call.\n * @throws CreditsError `CREDITS_BAD_AMOUNT`, `CREDITS_BAD_KEY` or `CREDITS_BAD_GRANT`\n */\nexport function checkNewGrant(kind: \"grant\" | \"purchase\", input: NewGrantInput, now: Date): void {\n checkAmount(input.amount, \"a grant\");\n checkKey(input.idempotencyKey);\n if (!input.accountId) throw new CreditsError(CREDITS_CODE.badGrant, \"A grant has an account\");\n if (!input.source || input.source.length > MAX_SOURCE)\n throw new CreditsError(CREDITS_CODE.badGrant, `A source is 1 to ${MAX_SOURCE} characters`);\n if (kind === \"grant\") checkGrantedTerms(input, now);\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,14 @@
1
1
  // src/port.ts
2
2
  var BUCKET = { granted: "granted", purchased: "purchased" };
3
3
  var ON_EXHAUSTED = { stop: "stop", fallbackPaid: "fallback_paid" };
4
- var ENTRY_TYPE = { grant: "GRANT", purchase: "PURCHASE", debit: "DEBIT", refund: "REFUND", expiry: "EXPIRY", adjustment: "ADJUSTMENT" };
4
+ var ENTRY_TYPE = {
5
+ grant: "GRANT",
6
+ purchase: "PURCHASE",
7
+ debit: "DEBIT",
8
+ refund: "REFUND",
9
+ expiry: "EXPIRY",
10
+ adjustment: "ADJUSTMENT"
11
+ };
5
12
  var HOLD_STATUS = { active: "active", settled: "settled", released: "released" };
6
13
  var CREDITS_CODE = {
7
14
  capDay: "CREDITS_CAP_DAY",
@@ -53,4 +60,4 @@ export {
53
60
  CREDITS_STATUS,
54
61
  CreditsError
55
62
  };
56
- //# sourceMappingURL=chunk-WDHQ7QHY.js.map
63
+ //# sourceMappingURL=chunk-NKDQNDSD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/port.ts"],"sourcesContent":["/**\n * The ports of « credits »: ONE ledger of usage credits per account. Generic: no product import, no database client (a `Query`\n * comes in), no wall clock (a `Clock` comes in). Credits are integers (never floats). The ledger is append-only and the balance is\n * DERIVED from it: nothing stores a balance that could drift.\n *\n * Two kinds of credit live in it. A GRANT is free usage (a coupon, a monthly allowance, a demo): it expires, may be capped per day or\n * month, and says what happens when it runs out (`stop`, or `fallback_paid`: go on with what the person bought). A PURCHASE is paid\n * usage: it does not expire and is never capped.\n */\nexport type { Clock, Query } from \"@krizaka/db\";\n\nexport const BUCKET = { granted: \"granted\", purchased: \"purchased\" } as const;\nexport type Bucket = (typeof BUCKET)[keyof typeof BUCKET];\n\n/** What happens when a grant has run out: refuse (`stop`), or move on to purchased credits (`fallback_paid`). */\nexport const ON_EXHAUSTED = { stop: \"stop\", fallbackPaid: \"fallback_paid\" } as const;\nexport type OnExhausted = (typeof ON_EXHAUSTED)[keyof typeof ON_EXHAUSTED];\n\n/** The kinds of line of the ledger. Amounts are signed: credits in are positive, credits out negative. */\nexport const ENTRY_TYPE = {\n grant: \"GRANT\",\n purchase: \"PURCHASE\",\n debit: \"DEBIT\",\n refund: \"REFUND\",\n expiry: \"EXPIRY\",\n adjustment: \"ADJUSTMENT\",\n} as const;\nexport type EntryType = (typeof ENTRY_TYPE)[keyof typeof ENTRY_TYPE];\n\nexport const HOLD_STATUS = { active: \"active\", settled: \"settled\", released: \"released\" } as const;\nexport type HoldStatus = (typeof HOLD_STATUS)[keyof typeof HOLD_STATUS];\n\n/** A pot of credits. Immutable: what is left of it is derived from the ledger. */\nexport interface Grant {\n id: string;\n accountId: string;\n bucket: Bucket;\n /** What it started with. */\n amount: number;\n /** The most it gives in a calendar day (in the settings' time zone), or none. Granted only. */\n capPerDay: number | null;\n /** The most it gives in a calendar month, or none. Granted only. */\n capPerMonth: number | null;\n /** When it lapses. Required for a granted pot, none for a purchased one. */\n expiresAt: Date | null;\n onExhausted: OnExhausted;\n /** Where it comes from (`coupon:<redemptionId>`, `plan:monthly`, `purchase:<paymentRef>`). */\n source: string;\n createdAt: Date;\n}\n\n/** One line of the ledger. */\nexport interface Entry {\n id: string;\n accountId: string;\n grantId: string | null;\n type: EntryType;\n /** Signed. */\n amount: number;\n reason: string;\n /** The caller's key for the whole operation: a replay finds its lines. */\n operation: string;\n /** Unique: `<operation>:<n>`. */\n idempotencyKey: string;\n /** For a REFUND: the operation it gives back. */\n reverses: string | null;\n holdId: string | null;\n at: Date;\n}\n\nexport interface Hold {\n id: string;\n accountId: string;\n amount: number;\n expiresAt: Date;\n status: HoldStatus;\n /** What it settled for, when settled. */\n settledAmount: number | null;\n createdAt: Date;\n closedAt: Date | null;\n operation: string;\n}\n\n/** A grant as the planner sees it: what is left, what it gave today and this month, and whether it was closed by an expiry line. */\nexport interface GrantState {\n grant: Grant;\n remaining: number;\n usedToday: number;\n usedThisMonth: number;\n /** An EXPIRY line closed it (revoked, or swept after its date). */\n closed: boolean;\n}\n\n/** The calendar the caps are counted in: the zone, and the local day and month of « now » in it (`2026-10-26`, `2026-10`). */\nexport interface Calendar {\n timeZone: string;\n day: string;\n month: string;\n}\n\n/** Where the ledger lives. Every adapter runs `creditStoreContract` (`@krizaka/credits/testkit`). Times come from the caller. */\nexport interface CreditStore {\n /** Serialises the writers of one account until the transaction ends (an advisory lock). Call it before reading to decide. */\n lock(accountId: string): Promise<void>;\n insertGrant(grant: Grant): Promise<void>;\n grantById(grantId: string): Promise<Grant | null>;\n /** The account's grants, with what is left and what each gave in the calendar's day and month. Includes lapsed and closed ones. */\n grantStates(accountId: string, calendar: Calendar): Promise<GrantState[]>;\n /** Writes lines. A duplicate `idempotencyKey` is a bug of the caller (it checked `entriesOf` first, under the lock) and throws. */\n insertEntries(entries: Entry[]): Promise<void>;\n /** The lines of one operation, in order. */\n entriesOf(operation: string): Promise<Entry[]>;\n /** The lines of the account, newest first. */\n history(accountId: string, options: { limit: number; before?: Date }): Promise<Entry[]>;\n insertHold(hold: Hold): Promise<void>;\n holdById(holdId: string): Promise<Hold | null>;\n holdByOperation(operation: string): Promise<Hold | null>;\n /** What the account's active holds that have not expired at `now` keep back, except `exceptHoldId`. */\n heldAmount(accountId: string, now: Date, exceptHoldId?: string): Promise<number>;\n closeHold(\n holdId: string,\n status: typeof HOLD_STATUS.settled | typeof HOLD_STATUS.released,\n settledAmount: number | null,\n at: Date,\n ): Promise<void>;\n /** Granted pots past their date that still hold credits and no EXPIRY line, oldest first. */\n lapsedWithCredits(now: Date, limit: number): Promise<{ grantId: string; accountId: string }[]>;\n}\n\n/** The knobs of the ledger, read from the admin's settings (`@krizaka/credits/settings`). */\nexport interface CreditsSettings {\n /** The IANA zone that days and months are counted in. */\n timeZone: string;\n /** Seconds a hold keeps credits back when the caller names no lifetime. */\n holdTtlSeconds: number;\n}\n\n/** Stable codes of the refusals of this package. */\nexport const CREDITS_CODE = {\n capDay: \"CREDITS_CAP_DAY\",\n capMonth: \"CREDITS_CAP_MONTH\",\n exhausted: \"CREDITS_EXHAUSTED\",\n paywall: \"PAYWALL\",\n badAmount: \"CREDITS_BAD_AMOUNT\",\n badGrant: \"CREDITS_BAD_GRANT\",\n badKey: \"CREDITS_BAD_KEY\",\n badSettings: \"CREDITS_BAD_SETTINGS\",\n unknownGrant: \"CREDITS_UNKNOWN_GRANT\",\n unknownHold: \"CREDITS_UNKNOWN_HOLD\",\n holdClosed: \"CREDITS_HOLD_CLOSED\",\n keyReused: \"CREDITS_KEY_REUSED\",\n nothingToRefund: \"CREDITS_NOTHING_TO_REFUND\",\n} as const;\nexport type CreditsCode = (typeof CREDITS_CODE)[keyof typeof CREDITS_CODE];\n\n/** The HTTP status a route may answer for each refusal. */\nexport const CREDITS_STATUS: Record<CreditsCode, number> = {\n CREDITS_CAP_DAY: 429,\n CREDITS_CAP_MONTH: 429,\n CREDITS_EXHAUSTED: 402,\n PAYWALL: 402,\n CREDITS_BAD_AMOUNT: 400,\n CREDITS_BAD_GRANT: 400,\n CREDITS_BAD_KEY: 400,\n CREDITS_BAD_SETTINGS: 500,\n CREDITS_UNKNOWN_GRANT: 404,\n CREDITS_UNKNOWN_HOLD: 404,\n CREDITS_HOLD_CLOSED: 409,\n CREDITS_KEY_REUSED: 409,\n CREDITS_NOTHING_TO_REFUND: 404,\n};\n\n/** A refusal: a stable `code`, the HTTP `status` a route may answer, and a message. */\nexport class CreditsError extends Error {\n readonly status: number;\n constructor(\n readonly code: CreditsCode,\n message: string,\n ) {\n super(message);\n this.name = \"CreditsError\";\n this.status = CREDITS_STATUS[code];\n }\n}\n"],"mappings":";AAWO,IAAM,SAAS,EAAE,SAAS,WAAW,WAAW,YAAY;AAI5D,IAAM,eAAe,EAAE,MAAM,QAAQ,cAAc,gBAAgB;AAInE,IAAM,aAAa;AAAA,EACxB,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AACd;AAGO,IAAM,cAAc,EAAE,QAAQ,UAAU,SAAS,WAAW,UAAU,WAAW;AA6GjF,IAAM,eAAe;AAAA,EAC1B,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,cAAc;AAAA,EACd,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,iBAAiB;AACnB;AAIO,IAAM,iBAA8C;AAAA,EACzD,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,sBAAsB;AAAA,EACtB,uBAAuB;AAAA,EACvB,sBAAsB;AAAA,EACtB,qBAAqB;AAAA,EACrB,oBAAoB;AAAA,EACpB,2BAA2B;AAC7B;AAGO,IAAM,eAAN,cAA2B,MAAM;AAAA,EAEtC,YACW,MACT,SACA;AACA,UAAM,OAAO;AAHJ;AAIT,SAAK,OAAO;AACZ,SAAK,SAAS,eAAe,IAAI;AAAA,EACnC;AAAA,EANW;AAAA,EAFF;AASX;","names":[]}