@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.
@@ -1,15 +1,17 @@
1
- import { L as LoadConfigOptions } from '../authHook-DUqyxueY.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-DUqyxueY.js';
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-CbMOXLBf.js';
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-AL8KSENV.js';
10
- export { A as AuthProvider, b as CapabilitiesContributor, P as ProvisionContext, d as ProvisionResult, c as createDatabase } from '../connection-AL8KSENV.js';
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
- export { r as runMigrations } from '../migrate-B4dwdtGP.js';
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
- export { CreateCoreAppOptions, Database, type FsProvisionerOptions, type IdempotencyEntry, type IdempotencyKeyStore, LoadConfigOptions, MailDeliveryError, type MailTransport, type PostSignupHookDeps, type RenderedEmail, type RoutesOptions, type RunCoreMigrationsFromEnvOptions, UserStore, WorkspaceProvisioner, WorkspaceRuntimeSandboxHandleStore, type WorkspaceRuntimeStoreLike, type WorkspaceSandboxHandleRecord, WorkspaceStore, coreConfigSchema, createCoreApp, createDrizzleIdempotencyStore, createFsProvisioner, createIdempotencyMiddleware, createMailTransport, createPostSignupHook, registerInviteRoutes, registerMemberRoutes, registerRoutes, registerSettingsRoutes, registerWorkspaceRoutes, renderMagicLink, renderResetPassword, renderVerifyEmail, renderWelcome, renderWorkspaceInvite, requireWorkspaceMember, runCoreMigrationsFromEnv, safeRedirect };
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 };