@hachej/boring-core 0.1.42 → 0.1.44
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/dist/PostgresMeteringStore-CzNv6xil.d.ts +224 -0
- package/dist/app/front/index.d.ts +216 -3
- package/dist/app/front/index.js +834 -43
- package/dist/app/server/index.d.ts +3 -3
- package/dist/app/server/index.js +33 -8
- package/dist/{authHook-DUqyxueY.d.ts → authHook-CzBsMwwM.d.ts} +2 -2
- package/dist/{chunk-C3YMOITB.js → chunk-I56OTSPB.js} +649 -6
- package/dist/{chunk-H5KU6R6Y.js → chunk-LIBHVT7V.js} +5 -1
- package/dist/{chunk-GZVKZD4P.js → chunk-UM5SHYIS.js} +11 -2
- package/dist/{chunk-MLTJKZL4.js → chunk-VYXEXOCO.js} +21 -10
- package/dist/{connection-AL8KSENV.d.ts → connection-C5SiqoNc.d.ts} +1 -1
- package/dist/front/index.d.ts +15 -2
- package/dist/front/index.js +2 -2
- package/dist/server/db/index.d.ts +4 -4
- package/dist/server/db/index.js +6 -2
- package/dist/server/index.d.ts +594 -7
- package/dist/server/index.js +1467 -4
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/{types-CbMOXLBf.d.ts → types-CWtJ4kgd.d.ts} +3 -0
- package/drizzle/0011_usage_metering.sql +57 -0
- package/drizzle/0012_credit_purchases.sql +9 -0
- package/drizzle/0013_credit_purchase_lifecycle.sql +28 -0
- package/drizzle/0014_reservation_charge_on_expire.sql +7 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +4 -4
- package/dist/migrate-B4dwdtGP.d.ts +0 -8
package/dist/server/index.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { L as LoadConfigOptions } from '../authHook-
|
|
2
|
-
export { A as AuthHookOptions, B as BetterAuthInstance, C as CreateAuthOptions, a as authHook, b as buildRuntimeConfigPayload, c as createAuth, l as loadConfig, v as validateConfig, d as validatePasswordStrength } from '../authHook-
|
|
1
|
+
import { L as LoadConfigOptions } from '../authHook-CzBsMwwM.js';
|
|
2
|
+
export { A as AuthHookOptions, B as BetterAuthInstance, C as CreateAuthOptions, a as authHook, b as buildRuntimeConfigPayload, c as createAuth, l as loadConfig, v as validateConfig, d as validatePasswordStrength } from '../authHook-CzBsMwwM.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { C as CoreConfig, M as MemberRole, d as WorkspaceRuntimeResourceSelector, e as WorkspaceRuntimeResource, f as WorkspaceRuntimeResourceInput } from '../types-
|
|
4
|
+
import { C as CoreConfig, M as MemberRole, d as WorkspaceRuntimeResourceSelector, e as WorkspaceRuntimeResource, f as WorkspaceRuntimeResourceInput } from '../types-CWtJ4kgd.js';
|
|
5
5
|
import * as fastify from 'fastify';
|
|
6
6
|
import { FastifyInstance, FastifyPluginAsync, preHandlerHookHandler, FastifyRequest, FastifyReply } from 'fastify';
|
|
7
7
|
import * as http from 'http';
|
|
8
8
|
import { IncomingMessage } from 'node:http';
|
|
9
|
-
import { C as CreateCoreAppOptions, D as Database, U as UserStore, W as WorkspaceStore, a as WorkspaceProvisioner } from '../connection-
|
|
10
|
-
export { A as AuthProvider, b as CapabilitiesContributor, P as ProvisionContext, d as ProvisionResult, c as createDatabase } from '../connection-
|
|
9
|
+
import { C as CreateCoreAppOptions, D as Database, U as UserStore, W as WorkspaceStore, a as WorkspaceProvisioner } from '../connection-C5SiqoNc.js';
|
|
10
|
+
export { A as AuthProvider, b as CapabilitiesContributor, P as ProvisionContext, d as ProvisionResult, c as createDatabase } from '../connection-C5SiqoNc.js';
|
|
11
11
|
import postgres from 'postgres';
|
|
12
|
-
|
|
12
|
+
import { P as PostgresMeteringStore, C as CreditLedgerEntry } from '../PostgresMeteringStore-CzNv6xil.js';
|
|
13
|
+
export { F as FinishReservationInput, G as GrantOnceInput, I as InsufficientCreditError, M as MeteringBalance, R as RecordUsageInput, a as RecordUsageResult, b as ReservationFinalStatus, c as ReserveInput, d as ReserveResult, r as runMigrations } from '../PostgresMeteringStore-CzNv6xil.js';
|
|
14
|
+
import { AgentMeteringSink } from '@hachej/boring-agent/server';
|
|
13
15
|
import 'better-auth';
|
|
14
16
|
import 'drizzle-orm/postgres-js';
|
|
15
17
|
|
|
@@ -419,4 +421,589 @@ declare class WorkspaceRuntimeSandboxHandleStore {
|
|
|
419
421
|
list(): Promise<WorkspaceSandboxHandleRecord[]>;
|
|
420
422
|
}
|
|
421
423
|
|
|
422
|
-
|
|
424
|
+
/**
|
|
425
|
+
* Token → credit pricing.
|
|
426
|
+
*
|
|
427
|
+
* Direct EU providers like Infomaniak return token usage but not a per-call
|
|
428
|
+
* cost, so we price from published per-token rates and apply a margin. The
|
|
429
|
+
* result is the credit amount to charge against the balance (in credit micros).
|
|
430
|
+
*
|
|
431
|
+
* Currency-neutral: rates are expressed in a "pricing currency unit" (e.g. EUR)
|
|
432
|
+
* per million tokens; `creditMicrosPerUnit` converts that to credit micros.
|
|
433
|
+
* With 1 credit = €0.000001, `creditMicrosPerUnit = 1_000_000`.
|
|
434
|
+
*/
|
|
435
|
+
interface ModelTokenRate {
|
|
436
|
+
/** Pricing-currency units per 1M input tokens (cache tokens billed as input). */
|
|
437
|
+
inputPerMillion: number;
|
|
438
|
+
/** Pricing-currency units per 1M output tokens. */
|
|
439
|
+
outputPerMillion: number;
|
|
440
|
+
}
|
|
441
|
+
interface CreditPricingConfig {
|
|
442
|
+
/** Multiplier applied on top of raw provider cost (your margin). 1.0 = at cost. */
|
|
443
|
+
margin: number;
|
|
444
|
+
/** Credit micros per 1 pricing-currency unit. 1 credit = €0.000001 ⇒ 1_000_000. */
|
|
445
|
+
creditMicrosPerUnit: number;
|
|
446
|
+
/** Ordered [pattern, rate] table; checked BEFORE the built-in
|
|
447
|
+
* DEFAULT_MODEL_RATES (configured rate wins for a model it matches), but the
|
|
448
|
+
* defaults still apply to models you didn't list — so a selectable expensive
|
|
449
|
+
* model (e.g. Claude Opus) is never silently dropped to the cheap fallback. */
|
|
450
|
+
rates?: Array<[RegExp, ModelTokenRate]>;
|
|
451
|
+
/**
|
|
452
|
+
* Rate applied when no pattern matches and the provider reported no cost.
|
|
453
|
+
* Defaults to CONSERVATIVE_DEFAULT_RATE so an unpriced model is never billed
|
|
454
|
+
* zero (free usage) in this prepaid path — set it explicitly to tune.
|
|
455
|
+
*/
|
|
456
|
+
defaultRate?: ModelTokenRate;
|
|
457
|
+
/**
|
|
458
|
+
* Trust a provider-reported cost over token pricing. Default false: for a
|
|
459
|
+
* prepaid credit balance, a provider cost in the wrong currency / stale test
|
|
460
|
+
* value / tiny non-zero number would undercharge, so we price from our own
|
|
461
|
+
* verified token rates with margin and ignore the reported cost.
|
|
462
|
+
*/
|
|
463
|
+
preferProviderReportedCost?: boolean;
|
|
464
|
+
}
|
|
465
|
+
/** Conservative fallback rate (≈ Claude Sonnet list price). EU open models are
|
|
466
|
+
* cheaper, so this over-charges an un-configured model rather than billing zero. */
|
|
467
|
+
declare const CONSERVATIVE_DEFAULT_RATE: ModelTokenRate;
|
|
468
|
+
declare const DEFAULT_MODEL_RATES: Array<[RegExp, ModelTokenRate]>;
|
|
469
|
+
interface CreditUsageInput {
|
|
470
|
+
inputTokens: number;
|
|
471
|
+
outputTokens: number;
|
|
472
|
+
cacheReadTokens?: number;
|
|
473
|
+
cacheWriteTokens?: number;
|
|
474
|
+
/** Provider-reported cost in pricing-currency units, if any (0 when absent). */
|
|
475
|
+
providerReportedCost?: number;
|
|
476
|
+
}
|
|
477
|
+
interface CreditCost {
|
|
478
|
+
inputTokens: number;
|
|
479
|
+
outputTokens: number;
|
|
480
|
+
cacheReadTokens: number;
|
|
481
|
+
cacheWriteTokens: number;
|
|
482
|
+
/** Raw provider cost (reported or estimated) in credit micros, before margin. */
|
|
483
|
+
providerCostMicros: number;
|
|
484
|
+
/** Amount to charge against the balance, in credit micros (with margin). */
|
|
485
|
+
billedCreditMicros: number;
|
|
486
|
+
/** True when no model rate matched and the conservative defaultRate was used —
|
|
487
|
+
* a signal to the operator to add an explicit rate for this model. */
|
|
488
|
+
pricedFromDefault: boolean;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The highest input/output rate this pricing config can actually charge: the max
|
|
492
|
+
* over the EFFECTIVE rate table (configured rates if set, else
|
|
493
|
+
* DEFAULT_MODEL_RATES) AND the conservative default fallback. Size a per-run
|
|
494
|
+
* reservation hold from this — never a hardcoded floor — or an expensive model
|
|
495
|
+
* in the table (e.g. Claude Opus at 15/75) can bill above a hold sized for a
|
|
496
|
+
* cheaper rate, defeating the hard stop.
|
|
497
|
+
*/
|
|
498
|
+
declare function maxEffectiveRate(config: CreditPricingConfig): ModelTokenRate;
|
|
499
|
+
/**
|
|
500
|
+
* The priciest rate over only the SERVED models (configured rates + the
|
|
501
|
+
* conservative default), EXCLUDING the built-in DEFAULT_MODEL_RATES. Use this to
|
|
502
|
+
* size the per-run hold: the hold should reflect the models this deployment
|
|
503
|
+
* actually serves (so a small starter grant stays usable), while billing an
|
|
504
|
+
* unmatched model still fails closed at maxEffectiveRate. An unreachable
|
|
505
|
+
* expensive model would overshoot the hold — bounded; the next run is refused.
|
|
506
|
+
*/
|
|
507
|
+
declare function maxServedRate(config: CreditPricingConfig): ModelTokenRate;
|
|
508
|
+
/** Estimate raw provider cost (pricing-currency units) from token counts. Cache
|
|
509
|
+
* tokens are billed at the input rate — a deliberate conservative over-count.
|
|
510
|
+
* Unmatched models fall back to `config.defaultRate` (never zero). */
|
|
511
|
+
declare function estimateProviderCost(modelId: string, inputTokens: number, outputTokens: number, config: CreditPricingConfig): {
|
|
512
|
+
units: number;
|
|
513
|
+
usedDefault: boolean;
|
|
514
|
+
};
|
|
515
|
+
/**
|
|
516
|
+
* Price one usage record into the credit amount to charge. Prefers a
|
|
517
|
+
* provider-reported cost; otherwise estimates from token rates. Applies the
|
|
518
|
+
* margin and converts to credit micros (rounded up so we never undercharge).
|
|
519
|
+
*/
|
|
520
|
+
declare function usageToCredits(usage: CreditUsageInput, model: {
|
|
521
|
+
provider?: string;
|
|
522
|
+
id?: string;
|
|
523
|
+
}, config: CreditPricingConfig): CreditCost;
|
|
524
|
+
|
|
525
|
+
declare const SIGNUP_GRANT_REASON = "signup_grant";
|
|
526
|
+
type CreditsLogger = (message: string, fields?: Record<string, unknown>) => void;
|
|
527
|
+
interface CreditsConfig {
|
|
528
|
+
enabled: boolean;
|
|
529
|
+
/** Free starter grant on signup, in credit micros (1 credit = €0.000001). */
|
|
530
|
+
signupGrantMicros: number;
|
|
531
|
+
/** Days until the signup grant expires; null = never. */
|
|
532
|
+
signupGrantExpiresAfterDays: number | null;
|
|
533
|
+
/**
|
|
534
|
+
* Per-run hold, in credit micros. This is the per-run overdraft bound: a run
|
|
535
|
+
* is admitted against this hold, but the actual charge is posted afterward, so
|
|
536
|
+
* a single run can overshoot the hold by (actualCost − hold). Set this to
|
|
537
|
+
* cover your worst-case single run (max tokens on the priciest enabled model)
|
|
538
|
+
* so the hard stop is effectively exact.
|
|
539
|
+
*/
|
|
540
|
+
runReservationMicros: number;
|
|
541
|
+
reservationTtlSeconds: number;
|
|
542
|
+
/** Floor below which a run is refused. */
|
|
543
|
+
minBalanceMicros: number;
|
|
544
|
+
pricing: CreditPricingConfig;
|
|
545
|
+
}
|
|
546
|
+
declare const DEFAULT_CREDITS_CONFIG: CreditsConfig;
|
|
547
|
+
interface CreditBalance {
|
|
548
|
+
enabled: boolean;
|
|
549
|
+
userId: string;
|
|
550
|
+
grantedMicros: number;
|
|
551
|
+
usedMicros: number;
|
|
552
|
+
remainingMicros: number;
|
|
553
|
+
activeReservedMicros: number;
|
|
554
|
+
/** remaining minus active holds; never negative in the response. */
|
|
555
|
+
availableMicros: number;
|
|
556
|
+
/** Owed amount when the ledger went negative (e.g. after a refund of spent
|
|
557
|
+
* credits); 0 otherwise. Surfaced for audit/support so debt isn't hidden. */
|
|
558
|
+
debtMicros: number;
|
|
559
|
+
currency: 'credits';
|
|
560
|
+
}
|
|
561
|
+
interface CreditUsageRecord {
|
|
562
|
+
usageId: string;
|
|
563
|
+
userId: string;
|
|
564
|
+
workspaceId?: string;
|
|
565
|
+
sessionId?: string;
|
|
566
|
+
runId?: string;
|
|
567
|
+
messageId?: string;
|
|
568
|
+
reservationId?: string;
|
|
569
|
+
provider?: string;
|
|
570
|
+
model?: string;
|
|
571
|
+
usage: {
|
|
572
|
+
input: number;
|
|
573
|
+
output: number;
|
|
574
|
+
cacheRead: number;
|
|
575
|
+
cacheWrite: number;
|
|
576
|
+
cost: {
|
|
577
|
+
total: number;
|
|
578
|
+
};
|
|
579
|
+
};
|
|
580
|
+
stopReason?: string;
|
|
581
|
+
}
|
|
582
|
+
declare class CreditExhaustedError extends Error {
|
|
583
|
+
readonly statusCode = 402;
|
|
584
|
+
readonly code = "PAYMENT_REQUIRED";
|
|
585
|
+
readonly details: {
|
|
586
|
+
balance: CreditBalance;
|
|
587
|
+
};
|
|
588
|
+
constructor(balance: CreditBalance);
|
|
589
|
+
}
|
|
590
|
+
/** The subset of PostgresMeteringStore the credits policy uses; Pick keeps
|
|
591
|
+
* upstream signature changes compile-time errors and lets tests stub it. */
|
|
592
|
+
type CreditsMeteringStore = Pick<PostgresMeteringStore, 'grantOnce' | 'grantPurchaseOnce' | 'revokePurchase' | 'getBalance' | 'reserve' | 'recordUsage' | 'finishReservation' | 'expireStaleReservations' | 'billedMicrosForRun' | 'billedMicrosForReservation' | 'markReservationFallbackCharge' | 'listLedger'>;
|
|
593
|
+
/**
|
|
594
|
+
* Product credit policy over the generic metering store: free signup grant,
|
|
595
|
+
* purchase grants, per-run reservation hard stop, token→credit pricing, and the
|
|
596
|
+
* balance shape the UI consumes. Currency-neutral (amounts are credit micros).
|
|
597
|
+
*/
|
|
598
|
+
declare class CreditsService {
|
|
599
|
+
private readonly store;
|
|
600
|
+
readonly config: CreditsConfig;
|
|
601
|
+
private readonly log?;
|
|
602
|
+
/** Users whose signup grant was ensured this process; avoids an INSERT per balance poll. */
|
|
603
|
+
private readonly signupGrantedUsers;
|
|
604
|
+
constructor(store: CreditsMeteringStore, config?: CreditsConfig, log?: CreditsLogger | undefined);
|
|
605
|
+
/** Idempotently grant the free starter credits (call from the post-signup hook
|
|
606
|
+
* and lazily on first balance/reserve). The grant NEVER expires: an expiring
|
|
607
|
+
* grant would drop from grantedMicros on expiry while spent usage stayed, turning
|
|
608
|
+
* a partly-spent trial into debt. (Proper expiry must cap/allocate usage against
|
|
609
|
+
* the promo balance — a tracked follow-up; the expiry config is rejected up front.) */
|
|
610
|
+
grantSignupCredits(userId: string): Promise<void>;
|
|
611
|
+
/** Credit a completed purchase. Globally idempotent per order id (safe on
|
|
612
|
+
* webhook retry, and the same order can never be credited to two users). The
|
|
613
|
+
* optional provider identity is persisted for audit/refund reconciliation. */
|
|
614
|
+
grantPurchase(userId: string, orderId: string, amountMicros: number, identity?: {
|
|
615
|
+
storeId?: string;
|
|
616
|
+
testMode?: boolean;
|
|
617
|
+
currency?: string;
|
|
618
|
+
variantId?: string;
|
|
619
|
+
}): Promise<{
|
|
620
|
+
created: boolean;
|
|
621
|
+
}>;
|
|
622
|
+
/** Revoke a refunded/disputed purchase. `refundFraction` is the cumulative
|
|
623
|
+
* fraction of the order refunded (LS refunded_amount / total) for partial
|
|
624
|
+
* refunds; omit for a full refund. `allowTombstone` permits writing a pre-grant
|
|
625
|
+
* refund tombstone for an order not yet credited (set only when the refund
|
|
626
|
+
* validates as a credit order); an already-credited order is always revocable.
|
|
627
|
+
* Idempotent per cumulative level. */
|
|
628
|
+
revokePurchase(orderId: string, opts?: {
|
|
629
|
+
refundFraction?: number;
|
|
630
|
+
allowTombstone?: boolean;
|
|
631
|
+
expectedStoreId?: string;
|
|
632
|
+
expectedTestMode?: boolean;
|
|
633
|
+
expectedCurrency?: string;
|
|
634
|
+
}): Promise<{
|
|
635
|
+
revoked: boolean;
|
|
636
|
+
}>;
|
|
637
|
+
getBalance(userId: string): Promise<CreditBalance>;
|
|
638
|
+
/** Recent credit ledger (grants/purchases + usage/refund debits) for the account
|
|
639
|
+
* activity view, newest first, capped (clamped 1..50 in the store). Empty when
|
|
640
|
+
* credits are disabled. */
|
|
641
|
+
listLedger(userId: string, limit?: number): Promise<CreditLedgerEntry[]>;
|
|
642
|
+
/** Reserve a per-run hold. Returns the reservation id; throws
|
|
643
|
+
* CreditExhaustedError (402) below the floor. */
|
|
644
|
+
reserveRun(input: {
|
|
645
|
+
userId: string;
|
|
646
|
+
workspaceId?: string;
|
|
647
|
+
sessionId?: string;
|
|
648
|
+
runId: string;
|
|
649
|
+
}): Promise<string | undefined>;
|
|
650
|
+
/** Charge native usage, priced token→credits with margin. Returns the billed
|
|
651
|
+
* credit micros (0 when disabled or the usage priced to nothing) so the caller can
|
|
652
|
+
* decide billability from the ACTUAL charge, not raw provider fields — e.g. a
|
|
653
|
+
* cost-only row prices to 0 unless preferProviderReportedCost is set. */
|
|
654
|
+
recordUsage(input: CreditUsageRecord): Promise<{
|
|
655
|
+
billedMicros: number;
|
|
656
|
+
}>;
|
|
657
|
+
settleRun(userId: string, runId: string, reservationId?: string): Promise<void>;
|
|
658
|
+
releaseRun(userId: string, runId: string, reservationId?: string): Promise<void>;
|
|
659
|
+
/**
|
|
660
|
+
* Fail-closed billing for a completed run whose usage write failed: a run that
|
|
661
|
+
* already executed must never go free. Charge the per-run hold (worst-case)
|
|
662
|
+
* as a conservative, idempotent debit, then settle the reservation. Tagged
|
|
663
|
+
* source 'pi-chat-fallback' so it's reconcilable against the missing real
|
|
664
|
+
* usage row. Over-charges rather than risk free usage.
|
|
665
|
+
*/
|
|
666
|
+
chargeFallbackUsage(input: {
|
|
667
|
+
userId: string;
|
|
668
|
+
runId: string;
|
|
669
|
+
reservationId?: string;
|
|
670
|
+
kind?: 'usage_write_failed' | 'no_billable_usage';
|
|
671
|
+
}): Promise<void>;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Adapt the credit policy to boring-agent's AgentMeteringSink. The service is
|
|
676
|
+
* resolved lazily because the sink is handed to createCoreWorkspaceAgentServer
|
|
677
|
+
* before the server (and its db) exists.
|
|
678
|
+
*
|
|
679
|
+
* reserveRun fails closed: 402 when credits are exhausted (CreditExhaustedError),
|
|
680
|
+
* 401 when an authenticated run has no user. recordUsage/settle/release are
|
|
681
|
+
* best-effort and skip silently for userless runs.
|
|
682
|
+
*/
|
|
683
|
+
declare function createCreditsMeteringSink(getService: () => CreditsService): AgentMeteringSink;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Lemon Squeezy (Merchant of Record) credit purchases.
|
|
687
|
+
*
|
|
688
|
+
* Product-neutral: this module verifies the webhook, parses the order, and
|
|
689
|
+
* grants credits via a host-supplied grant function. The host owns how many
|
|
690
|
+
* credits an order is worth (pricing/bonus policy) and which user it belongs
|
|
691
|
+
* to. Grants are idempotent per order id, so webhook retries never double-credit.
|
|
692
|
+
*
|
|
693
|
+
* Lemon Squeezy signs webhooks with HMAC-SHA256 of the raw request body using
|
|
694
|
+
* the store's signing secret, in the `X-Signature` header.
|
|
695
|
+
*/
|
|
696
|
+
/** Verify the `X-Signature` HMAC against the raw request body. Timing-safe. */
|
|
697
|
+
declare function verifyLemonSqueezySignature(rawBody: string | Buffer, signatureHeader: string | undefined, secret: string): boolean;
|
|
698
|
+
/** Server-signed attribution token binding a user id to a server-created
|
|
699
|
+
* checkout. Set as `custom_data.uat`; verified on the webhook so a buyer-crafted
|
|
700
|
+
* hosted-checkout URL can't attribute a paid order to an arbitrary account. */
|
|
701
|
+
declare function signUserAttribution(userId: string, secret: string): string;
|
|
702
|
+
/** Verify the token against the signing secret, or ANY of several secrets (current
|
|
703
|
+
* + previous) to allow secret rotation without breaking in-flight checkout links. */
|
|
704
|
+
declare function verifyUserAttribution(userId: string | undefined, token: string | undefined, secret: string | readonly string[]): boolean;
|
|
705
|
+
interface LemonSqueezyOrder {
|
|
706
|
+
eventName: string;
|
|
707
|
+
orderId: string;
|
|
708
|
+
/** From checkout `custom_data.user_id` — who to credit. */
|
|
709
|
+
userId?: string;
|
|
710
|
+
/** From checkout `custom_data.uat` — server-signed HMAC binding the user_id to
|
|
711
|
+
* a server-created checkout (a crafted hosted-checkout URL can't forge it). */
|
|
712
|
+
userAttributionToken?: string;
|
|
713
|
+
userEmail?: string;
|
|
714
|
+
status?: string;
|
|
715
|
+
/** Test/live mode. `undefined` when the payload omitted it — treated as a
|
|
716
|
+
* MISMATCH by isCreditOrder (never silently assumed live). */
|
|
717
|
+
testMode?: boolean;
|
|
718
|
+
/** Lemon Squeezy store the order belongs to. */
|
|
719
|
+
storeId?: string;
|
|
720
|
+
currency?: string;
|
|
721
|
+
/** Pre-tax order amount in cents. `undefined` if the field was ABSENT (vs a real 0)
|
|
722
|
+
* so the underpayment check can fail closed on a missing required money field. */
|
|
723
|
+
subtotalCents: number | undefined;
|
|
724
|
+
/** Discount applied, pre-tax, in cents. Net paid pre-tax = subtotal − discount.
|
|
725
|
+
* `undefined` if absent (presence-preserving — a missing discount must not look
|
|
726
|
+
* like a real 0, which could over-credit a discounted order). */
|
|
727
|
+
discountTotalCents: number | undefined;
|
|
728
|
+
/** Tax-inclusive total in cents (MoR adds VAT here). `undefined` if absent. */
|
|
729
|
+
totalCents: number | undefined;
|
|
730
|
+
/** Tax amount in cents (0 when no tax). Net paid pre-tax also = total − tax, an
|
|
731
|
+
* independent cross-check against subtotal − discount. */
|
|
732
|
+
taxCents: number;
|
|
733
|
+
/** Whether the order has been (fully or partially) refunded. */
|
|
734
|
+
refunded: boolean;
|
|
735
|
+
/** Cumulative amount refunded so far, tax-inclusive, in cents. */
|
|
736
|
+
refundedAmountCents: number;
|
|
737
|
+
variantId?: string;
|
|
738
|
+
/** Units of the pack purchased (default 1). Credits scale with it. */
|
|
739
|
+
quantity: number;
|
|
740
|
+
productName?: string;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Parse a Lemon Squeezy webhook payload into a normalized order. Returns null
|
|
744
|
+
* for non-order or malformed payloads.
|
|
745
|
+
*/
|
|
746
|
+
declare function parseLemonSqueezyOrder(payload: unknown): LemonSqueezyOrder | null;
|
|
747
|
+
interface LemonSqueezyWebhookOptions {
|
|
748
|
+
secret: string;
|
|
749
|
+
/** Credit amount (micros of your credit unit) to grant for this order. */
|
|
750
|
+
creditsForOrder: (order: LemonSqueezyOrder) => number;
|
|
751
|
+
/**
|
|
752
|
+
* Resolve the user to credit. Defaults to `order.userId` (custom_data).
|
|
753
|
+
* SECURITY: only trust custom_data.user_id when checkouts are created
|
|
754
|
+
* SERVER-side (see createLemonSqueezyCheckout) so the id is set by your
|
|
755
|
+
* server, not a client-editable hosted-checkout URL. Because the server sets
|
|
756
|
+
* a deterministic user per order, `purchase:<orderId>` is then effectively a
|
|
757
|
+
* per-order idempotency key (the same order never maps to two users).
|
|
758
|
+
*/
|
|
759
|
+
resolveUserId?: (order: LemonSqueezyOrder) => string | undefined;
|
|
760
|
+
/** Optional: returns whether the resolved user still exists. When provided, a credit
|
|
761
|
+
* order for a non-existent (deleted) user is 200-acked WITHOUT granting, so a stale
|
|
762
|
+
* webhook can't resurrect a deleted user's purchase/grant rows (PII). */
|
|
763
|
+
userExists?: (userId: string) => Promise<boolean>;
|
|
764
|
+
/** When set, the order's `custom_data.uat` MUST be a valid attribution token
|
|
765
|
+
* for its user_id (signUserAttribution) or the order is not credited — binds
|
|
766
|
+
* attribution to a server-created checkout, not a buyer-supplied user_id. May be
|
|
767
|
+
* an array (current + previous secrets) to allow rotation: a token signed with any
|
|
768
|
+
* listed secret verifies, so in-flight checkout links survive a secret rotation. */
|
|
769
|
+
attributionSecret?: string | readonly string[];
|
|
770
|
+
/** Grant credits idempotently. `reason` is `purchase:<orderId>` (the
|
|
771
|
+
* idempotency key); `orderId` is provided so callers don't re-parse it. The
|
|
772
|
+
* full `order` is passed so the grant can persist provider identity. */
|
|
773
|
+
grant: (input: {
|
|
774
|
+
userId: string;
|
|
775
|
+
orderId: string;
|
|
776
|
+
reason: string;
|
|
777
|
+
amountMicros: number;
|
|
778
|
+
}, order: LemonSqueezyOrder) => Promise<{
|
|
779
|
+
created: boolean;
|
|
780
|
+
}>;
|
|
781
|
+
/** Which events to credit on. Defaults to `order_created`. */
|
|
782
|
+
creditableEvents?: string[];
|
|
783
|
+
/**
|
|
784
|
+
* Confirm this paid order is actually a credit-pack purchase before granting
|
|
785
|
+
* (currency, mode, and that the variant is a configured pack). REQUIRED:
|
|
786
|
+
* without it, ANY signed paid order on the store would mint credits. Returning
|
|
787
|
+
* false acks the webhook without crediting.
|
|
788
|
+
*/
|
|
789
|
+
isCreditOrder: (order: LemonSqueezyOrder) => boolean;
|
|
790
|
+
/** Optional STRICT check: is this order on OUR store/mode/currency (ignoring the
|
|
791
|
+
* variant, all fields required)? Provide it ONLY for a credit-only store: then a
|
|
792
|
+
* paid order that's ours but NOT a credit order (unknown/misconfigured variant)
|
|
793
|
+
* returns a retryable 500 instead of a 200 ack — so a paid customer isn't left
|
|
794
|
+
* without credits. Omit it for a MIXED store (credits + other products), so a
|
|
795
|
+
* legitimate non-credit order is 200-ignored rather than retried/alerted forever. */
|
|
796
|
+
isOurStoreOrder?: (order: LemonSqueezyOrder) => boolean;
|
|
797
|
+
/** Optional LENIENT check for REFUNDS: a refund payload may omit store/mode/
|
|
798
|
+
* currency, so a missing field passes and only a present-and-mismatched field
|
|
799
|
+
* rejects. A refund failing this is ignored (can't revoke a credited order by
|
|
800
|
+
* order id alone). Defaults to always-true when omitted. */
|
|
801
|
+
isRefundForOurStore?: (order: LemonSqueezyOrder) => boolean;
|
|
802
|
+
/** Optional check: is this a paid order for a KNOWN credit-pack variant whose
|
|
803
|
+
* store/mode/currency identity is INCOMPLETE (a required field is missing rather
|
|
804
|
+
* than present-and-mismatched)? When true, the webhook returns a retryable 500
|
|
805
|
+
* instead of a silent 200 `not_a_credit_order` — a recognized paid pack we can't
|
|
806
|
+
* safely attribute must fail loud (parser gap / LS payload change), not drop the
|
|
807
|
+
* customer's credits. A genuinely foreign order (present field contradicts our
|
|
808
|
+
* config) is NOT this case and is still 200-ignored. */
|
|
809
|
+
isUnverifiedCreditOrder?: (order: LemonSqueezyOrder) => boolean;
|
|
810
|
+
/** Credit micros per 1 currency unit (e.g. 1_000_000 = €0.000001/credit). When
|
|
811
|
+
* set, the webhook refuses to mint a fixed pack value unless the net paid
|
|
812
|
+
* amount (subtotal − discount) covers it — so a dashboard/manual discount or LS
|
|
813
|
+
* bug can't grant full credits for an underpaid order. */
|
|
814
|
+
creditMicrosPerUnit?: number;
|
|
815
|
+
/** Events that revoke a previously-credited purchase. Default `order_refunded`. */
|
|
816
|
+
refundEvents?: string[];
|
|
817
|
+
/** Revoke a refunded/disputed order's credits (idempotent per order). REQUIRED
|
|
818
|
+
* so a refund is never silently dropped (which would leave a refunded order
|
|
819
|
+
* credited). */
|
|
820
|
+
onRefund: (order: LemonSqueezyOrder) => Promise<{
|
|
821
|
+
revoked: boolean;
|
|
822
|
+
}>;
|
|
823
|
+
log?: (message: string, fields?: Record<string, unknown>) => void;
|
|
824
|
+
}
|
|
825
|
+
interface LemonSqueezyWebhookResult {
|
|
826
|
+
status: number;
|
|
827
|
+
body: {
|
|
828
|
+
ok: boolean;
|
|
829
|
+
reason?: string;
|
|
830
|
+
orderId?: string;
|
|
831
|
+
created?: boolean;
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Full webhook handler: verify signature → parse → grant. Framework-agnostic
|
|
836
|
+
* (takes the raw body + header) so the host wires it to any router with raw-body
|
|
837
|
+
* access. Returns the HTTP status + JSON body to send.
|
|
838
|
+
*/
|
|
839
|
+
declare function handleLemonSqueezyWebhook(rawBody: string | Buffer, signatureHeader: string | undefined, options: LemonSqueezyWebhookOptions): Promise<LemonSqueezyWebhookResult>;
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Server-side Lemon Squeezy checkout creation.
|
|
843
|
+
*
|
|
844
|
+
* The buyer's user id is set HERE (from the authenticated session), not by the
|
|
845
|
+
* browser. The purchase webhook then trusts `custom_data.user_id` because the
|
|
846
|
+
* server — not a client-editable URL — put it there. This is the money-safe
|
|
847
|
+
* alternative to client-built hosted-checkout links.
|
|
848
|
+
*/
|
|
849
|
+
interface CreateCheckoutInput {
|
|
850
|
+
apiKey: string;
|
|
851
|
+
storeId: string;
|
|
852
|
+
variantId: string;
|
|
853
|
+
/** Authenticated user id — written into checkout custom data server-side. */
|
|
854
|
+
userId: string;
|
|
855
|
+
/** Secret used to sign the user attribution token (custom_data.uat) so the
|
|
856
|
+
* webhook can verify the user_id came from this server-created checkout. */
|
|
857
|
+
attributionSecret?: string;
|
|
858
|
+
email?: string;
|
|
859
|
+
/** Where Lemon Squeezy redirects after a successful purchase. */
|
|
860
|
+
redirectUrl?: string;
|
|
861
|
+
/** Use test-mode checkout (mirrors the API key's mode). */
|
|
862
|
+
testMode?: boolean;
|
|
863
|
+
}
|
|
864
|
+
type FetchLike = (url: string, init: {
|
|
865
|
+
method: string;
|
|
866
|
+
headers: Record<string, string>;
|
|
867
|
+
body: string;
|
|
868
|
+
}) => Promise<{
|
|
869
|
+
ok: boolean;
|
|
870
|
+
status: number;
|
|
871
|
+
json: () => Promise<unknown>;
|
|
872
|
+
text: () => Promise<string>;
|
|
873
|
+
}>;
|
|
874
|
+
/** Build the JSON:API request body for a checkout. Exported for testing. */
|
|
875
|
+
declare function buildCheckoutRequestBody(input: CreateCheckoutInput): Record<string, unknown>;
|
|
876
|
+
/** Create a hosted checkout and return its URL. Throws on API failure. */
|
|
877
|
+
declare function createLemonSqueezyCheckout(input: CreateCheckoutInput, fetchImpl?: FetchLike): Promise<{
|
|
878
|
+
url: string;
|
|
879
|
+
}>;
|
|
880
|
+
|
|
881
|
+
interface LemonSqueezyCheckoutConfig {
|
|
882
|
+
apiKey: string;
|
|
883
|
+
storeId: string;
|
|
884
|
+
/** Pack variants keyed by a short pack id the client requests (e.g. "10","25"). */
|
|
885
|
+
variants: Record<string, string>;
|
|
886
|
+
/** Pack id used when the request names none. */
|
|
887
|
+
defaultPack: string;
|
|
888
|
+
redirectUrl?: string;
|
|
889
|
+
testMode?: boolean;
|
|
890
|
+
}
|
|
891
|
+
interface LemonSqueezyRouteOptions {
|
|
892
|
+
webhookSecret: string;
|
|
893
|
+
/**
|
|
894
|
+
* Secret used to sign/verify the checkout attribution token (`custom_data.uat`).
|
|
895
|
+
* Defaults to `webhookSecret`, but SHOULD be a dedicated, stable secret so rotating
|
|
896
|
+
* the LS webhook secret doesn't invalidate in-flight checkout links. Provide an
|
|
897
|
+
* array (current first, then previous secrets) to allow rotation: checkouts sign
|
|
898
|
+
* with the first; the webhook verifies against ANY, so in-flight links survive.
|
|
899
|
+
*/
|
|
900
|
+
attributionSecret?: string | readonly string[];
|
|
901
|
+
/** Optional: resolve whether a credited user still exists. When provided, a credit
|
|
902
|
+
* order for a deleted user is 200-acked without granting (no PII resurrection via a
|
|
903
|
+
* stale webhook). */
|
|
904
|
+
userExists?: (userId: string) => Promise<boolean>;
|
|
905
|
+
/**
|
|
906
|
+
* Variant ids that are credit packs. REQUIRED and non-empty for the webhook
|
|
907
|
+
* to credit anything — only orders for these variants mint credits, so
|
|
908
|
+
* unrelated products on the same store are ignored (fail closed).
|
|
909
|
+
*/
|
|
910
|
+
creditVariantIds: string[];
|
|
911
|
+
/** Expected mode of credit orders: true = test, false = live. An order whose
|
|
912
|
+
* test_mode differs is ignored (prevents test↔live cross-crediting). */
|
|
913
|
+
expectedTestMode: boolean;
|
|
914
|
+
/** Expected Lemon Squeezy store id. When set, an order from another store is
|
|
915
|
+
* ignored (defense in depth on top of the per-store webhook secret). */
|
|
916
|
+
expectedStoreId?: string;
|
|
917
|
+
/**
|
|
918
|
+
* Whether this store sells ONLY credit packs. **Defaults to true (fail-closed).**
|
|
919
|
+
* When true (a credit-only store), a paid order in our store/mode/currency whose
|
|
920
|
+
* variant isn't a configured credit pack is treated as a pack MISCONFIGURATION and
|
|
921
|
+
* returns a retryable 500 (the customer paid and would otherwise get nothing — a
|
|
922
|
+
* visible, recoverable failure rather than a silent drop). Set **false** for a MIXED
|
|
923
|
+
* store selling credits plus other products: such an order is then a different product
|
|
924
|
+
* and is 200-ignored, so its webhook isn't retried/alerted forever. A known credit
|
|
925
|
+
* variant with incomplete identity always 500s regardless (see isUnverifiedCreditOrder).
|
|
926
|
+
*/
|
|
927
|
+
creditOnlyStore?: boolean;
|
|
928
|
+
/** Currency a paid order must be in to be credited (default 'EUR'). A missing
|
|
929
|
+
* or mismatched currency is rejected. */
|
|
930
|
+
requireCurrency?: string;
|
|
931
|
+
/**
|
|
932
|
+
* Fixed credit micros to grant per credit-pack variant id. REQUIRED, and the
|
|
933
|
+
* ONLY crediting basis — never order-amount math: the grant is the pack's
|
|
934
|
+
* configured value, so a multi-item order, a discount, or a tax change can't
|
|
935
|
+
* change how many credits are minted. Every entry in `creditVariantIds` must
|
|
936
|
+
* have a positive value here (enforced at registration). A non-pack host that
|
|
937
|
+
* needs a different policy must add an explicit, separately-tested route — the
|
|
938
|
+
* money webhook keeps exactly one safe path.
|
|
939
|
+
*/
|
|
940
|
+
creditMicrosByVariant: Record<string, number>;
|
|
941
|
+
webhookPath?: string;
|
|
942
|
+
/** Server-side checkout creation. Required for money-safe buyer attribution
|
|
943
|
+
* (the user id is set server-side, not by the browser). */
|
|
944
|
+
checkout?: LemonSqueezyCheckoutConfig;
|
|
945
|
+
checkoutPath?: string;
|
|
946
|
+
}
|
|
947
|
+
interface StripeCheckoutConfig {
|
|
948
|
+
/** Stripe secret key (sk_… or rk_…). */
|
|
949
|
+
apiKey: string;
|
|
950
|
+
/** Fixed packs keyed by a short pack id the client requests (e.g. "10","25") → Stripe Price id. */
|
|
951
|
+
variants: Record<string, string>;
|
|
952
|
+
/** Pack id used when the request names none. */
|
|
953
|
+
defaultPack: string;
|
|
954
|
+
/** Stripe Price id of the custom (pay-what-you-want) pack, for creating its checkout.
|
|
955
|
+
* The custom pack's webhook policy (id, minimum) lives on StripeRouteOptions.customPack. */
|
|
956
|
+
customPriceId?: string;
|
|
957
|
+
redirectUrl?: string;
|
|
958
|
+
}
|
|
959
|
+
interface StripeRouteOptions {
|
|
960
|
+
/** Webhook endpoint signing secret (whsec_…). When omitted, the webhook route is NOT
|
|
961
|
+
* registered (checkout can still open, but purchases won't auto-credit). */
|
|
962
|
+
webhookSecret?: string;
|
|
963
|
+
/** Secret(s) to sign/verify the attribution token (metadata.uat). Defaults to the
|
|
964
|
+
* webhook secret. Provide [current, ...previous] so rotating the webhook secret doesn't
|
|
965
|
+
* reject in-flight checkouts: sessions sign with the first; the webhook verifies any. */
|
|
966
|
+
attributionSecret?: string | readonly string[];
|
|
967
|
+
/** true = test mode. The webhook only credits sessions whose livemode matches. */
|
|
968
|
+
expectedTestMode: boolean;
|
|
969
|
+
/** Currency a paid session must be in to be credited (default 'EUR'). */
|
|
970
|
+
requireCurrency?: string;
|
|
971
|
+
/** Whether the account sells only credit packs (default true → an unknown-pack PAID
|
|
972
|
+
* session is a misconfig that fails loud rather than a silent drop). */
|
|
973
|
+
creditOnlyStore?: boolean;
|
|
974
|
+
/** Resolve whether a credited user still exists (no PII resurrection on a stale webhook). */
|
|
975
|
+
userExists?: (userId: string) => Promise<boolean>;
|
|
976
|
+
/** Fixed pack id → credit micros (the authoritative value the webhook grants). The
|
|
977
|
+
* custom pack is credited from the amount paid, not this map. */
|
|
978
|
+
creditMicrosByPack: Record<string, number>;
|
|
979
|
+
/** Pay-what-you-want pack WEBHOOK policy — its reserved id and minimum (minor units).
|
|
980
|
+
* Top-level (not under checkout) so a paid custom session is still recognized/credited
|
|
981
|
+
* even if checkout creation is temporarily unconfigured. The price id for CREATING the
|
|
982
|
+
* checkout lives at checkout.customPriceId. */
|
|
983
|
+
customPack?: {
|
|
984
|
+
id: string;
|
|
985
|
+
minMinor: number;
|
|
986
|
+
};
|
|
987
|
+
checkout?: StripeCheckoutConfig;
|
|
988
|
+
checkoutPath?: string;
|
|
989
|
+
webhookPath?: string;
|
|
990
|
+
}
|
|
991
|
+
interface CreditsRoutesOptions {
|
|
992
|
+
service: CreditsService;
|
|
993
|
+
/** Resolve the authenticated user id. Default: `request.user?.id`. */
|
|
994
|
+
getUserId?: (request: FastifyRequest) => string | undefined;
|
|
995
|
+
balancePath?: string;
|
|
996
|
+
historyPath?: string;
|
|
997
|
+
/** Configure at most ONE purchase provider. */
|
|
998
|
+
lemonSqueezy?: LemonSqueezyRouteOptions;
|
|
999
|
+
stripe?: StripeRouteOptions;
|
|
1000
|
+
log?: (message: string, fields?: Record<string, unknown>) => void;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Register the credit balance endpoint and (optionally) the Lemon Squeezy
|
|
1004
|
+
* purchase webhook. The webhook route reads the RAW request body so the HMAC
|
|
1005
|
+
* signature can be verified before parsing.
|
|
1006
|
+
*/
|
|
1007
|
+
declare function registerCreditsRoutes(app: FastifyInstance, options: CreditsRoutesOptions): void;
|
|
1008
|
+
|
|
1009
|
+
export { CONSERVATIVE_DEFAULT_RATE, type CreateCheckoutInput, CreateCoreAppOptions, type CreditBalance, type CreditCost, CreditExhaustedError, type CreditPricingConfig, type CreditUsageInput, type CreditUsageRecord, type CreditsConfig, type CreditsMeteringStore, type CreditsRoutesOptions, CreditsService, DEFAULT_CREDITS_CONFIG, DEFAULT_MODEL_RATES, Database, type FsProvisionerOptions, type IdempotencyEntry, type IdempotencyKeyStore, type LemonSqueezyCheckoutConfig, type LemonSqueezyOrder, type LemonSqueezyRouteOptions, type LemonSqueezyWebhookOptions, type LemonSqueezyWebhookResult, LoadConfigOptions, MailDeliveryError, type MailTransport, type ModelTokenRate, type PostSignupHookDeps, PostgresMeteringStore, type RenderedEmail, type RoutesOptions, type RunCoreMigrationsFromEnvOptions, SIGNUP_GRANT_REASON, UserStore, WorkspaceProvisioner, WorkspaceRuntimeSandboxHandleStore, type WorkspaceRuntimeStoreLike, type WorkspaceSandboxHandleRecord, WorkspaceStore, buildCheckoutRequestBody, coreConfigSchema, createCoreApp, createCreditsMeteringSink, createDrizzleIdempotencyStore, createFsProvisioner, createIdempotencyMiddleware, createLemonSqueezyCheckout, createMailTransport, createPostSignupHook, estimateProviderCost, handleLemonSqueezyWebhook, maxEffectiveRate, maxServedRate, parseLemonSqueezyOrder, registerCreditsRoutes, registerInviteRoutes, registerMemberRoutes, registerRoutes, registerSettingsRoutes, registerWorkspaceRoutes, renderMagicLink, renderResetPassword, renderVerifyEmail, renderWelcome, renderWorkspaceInvite, requireWorkspaceMember, runCoreMigrationsFromEnv, safeRedirect, signUserAttribution, usageToCredits, verifyLemonSqueezySignature, verifyUserAttribution };
|