@pear-protocol/core-sdk 1.20.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -187,7 +187,7 @@ declare class Core {
187
187
  get: (query?: HttpTypes.QuoteFeeParamsInput, headers?: ClientHeaders) => Promise<{
188
188
  baseBps: string;
189
189
  discounts: {
190
- scope: "connector" | "all" | "user" | "client_id";
190
+ scope: "user" | "connector" | "all" | "client_id";
191
191
  rate: string;
192
192
  }[];
193
193
  finalBps: string;
@@ -3471,16 +3471,19 @@ declare class Core {
3471
3471
  }[];
3472
3472
  }>;
3473
3473
  /**
3474
- * Closed-trade history, newest close first, composed server-side.
3474
+ * Closed-position history, newest close first, composed server-side.
3475
3475
  *
3476
- * A page may hold fewer than `limit` trades and still return a `nextCursor` —
3476
+ * A page may hold fewer than `limit` positions and still return a `nextCursor` —
3477
3477
  * page until `nextCursor` is absent, never until a page comes back short.
3478
+ * `includeSteps` is accepted only together with `positionId`: the timeline is
3479
+ * unbounded per row, so it is served one position at a time.
3478
3480
  */
3479
3481
  closed: (query?: HttpTypes.ClosedPositionListParamsInput, headers?: ClientHeaders) => Promise<{
3480
3482
  limit: number;
3481
- trades: {
3483
+ positions: {
3482
3484
  positionId: string;
3483
3485
  connector: "hyperliquid" | "binance" | "bybit" | "okx" | "lighter";
3486
+ status: "CLOSED";
3484
3487
  openedAt: string;
3485
3488
  closedAt: string;
3486
3489
  origin: "PARTIAL" | "PEAR" | "EXTERNAL";
@@ -3547,180 +3550,369 @@ declare class Core {
3547
3550
  }[];
3548
3551
  nextCursor?: string | undefined;
3549
3552
  }>;
3553
+ /**
3554
+ * Open-position history: the realized part of every open position, newest
3555
+ * opened first, composed server-side. Every open position yields exactly
3556
+ * one row — one whose fills are not visible yet comes back as an explicit
3557
+ * zero-state row.
3558
+ *
3559
+ * Page until `nextCursor` is absent, never until a page comes back short.
3560
+ */
3561
+ open: (query?: HttpTypes.OpenPositionListParamsInput, headers?: ClientHeaders) => Promise<{
3562
+ limit: number;
3563
+ positions: {
3564
+ positionId: string;
3565
+ connector: "hyperliquid" | "binance" | "bybit" | "okx" | "lighter";
3566
+ status: "OPEN";
3567
+ openedAt: string;
3568
+ updatedAt: string;
3569
+ origin: "PARTIAL" | "PEAR" | "EXTERNAL";
3570
+ realizedGrossPnl: `-${string}` | `+${string}`;
3571
+ netCashflow: `-${string}` | `+${string}`;
3572
+ totalFees: `-${string}` | `+${string}`;
3573
+ tradeFees: `-${string}` | `+${string}`;
3574
+ pearFees: `-${string}` | `+${string}`;
3575
+ totalFunding: `-${string}` | `+${string}`;
3576
+ initialExposureUSD: Record<string, `-${string}` | `+${string}`>;
3577
+ hasSyntheticFills: boolean;
3578
+ hasProvisionalFills: boolean;
3579
+ unstableHistoricalData: boolean;
3580
+ legs: {
3581
+ asset: string;
3582
+ side: "LONG" | "SHORT";
3583
+ realized: {
3584
+ quantity: string;
3585
+ unpricedQuantity: string;
3586
+ grossPnl: `-${string}` | `+${string}`;
3587
+ netPnl: `-${string}` | `+${string}`;
3588
+ exitVwap?: string | undefined;
3589
+ };
3590
+ remaining: {
3591
+ quantity: `-${string}` | `+${string}`;
3592
+ entryVwap?: string | undefined;
3593
+ };
3594
+ totalFees: `-${string}` | `+${string}`;
3595
+ tradeFees: `-${string}` | `+${string}`;
3596
+ pearFees: `-${string}` | `+${string}`;
3597
+ funding: `-${string}` | `+${string}`;
3598
+ }[];
3599
+ steps?: {
3600
+ at: string;
3601
+ grossPnl: `-${string}` | `+${string}`;
3602
+ legs: ({
3603
+ kind: "INCREASE";
3604
+ asset: string;
3605
+ side: "BUY" | "SELL";
3606
+ quantity: string;
3607
+ price: string;
3608
+ tradeFee: `-${string}` | `+${string}`;
3609
+ pearFee: `-${string}` | `+${string}`;
3610
+ } | {
3611
+ kind: "REDUCE";
3612
+ asset: string;
3613
+ side: "BUY" | "SELL";
3614
+ quantity: string;
3615
+ price: string;
3616
+ entryBasis: string;
3617
+ grossPnl: `-${string}` | `+${string}`;
3618
+ tradeFee: `-${string}` | `+${string}`;
3619
+ pearFee: `-${string}` | `+${string}`;
3620
+ } | {
3621
+ kind: "REDUCE_UNPRICED";
3622
+ asset: string;
3623
+ side: "BUY" | "SELL";
3624
+ quantity: string;
3625
+ tradeFee: `-${string}` | `+${string}`;
3626
+ pearFee: `-${string}` | `+${string}`;
3627
+ })[];
3628
+ executionId?: string | undefined;
3629
+ }[] | undefined;
3630
+ }[];
3631
+ nextCursor?: string | undefined;
3632
+ }>;
3633
+ };
3634
+ /** Fill-level history; basket commands live under the singular `trade` namespace. */
3635
+ trades: {
3636
+ /** List fill-level trades grouped by execution with compound cursor pagination. */
3637
+ list: (query?: HttpTypes.TradeListParamsInput, headers?: ClientHeaders) => Promise<{
3638
+ limit: number;
3639
+ groups: {
3640
+ id: string;
3641
+ connector: "hyperliquid" | "binance" | "bybit" | "okx" | "lighter";
3642
+ at: string;
3643
+ netCashflow: `-${string}` | `+${string}`;
3644
+ trades: {
3645
+ id: string;
3646
+ positionId: string;
3647
+ symbol: string;
3648
+ connector: "hyperliquid" | "binance" | "bybit" | "okx" | "lighter";
3649
+ side: "BUY" | "SELL";
3650
+ quantity: string;
3651
+ price: string;
3652
+ timestamp: string;
3653
+ settlement: "PROVISIONAL" | "CONFIRMED";
3654
+ positionEffect: {
3655
+ kind: "OPEN";
3656
+ openedQuantity: string;
3657
+ } | {
3658
+ kind: "INCREASE";
3659
+ increasedQuantity: string;
3660
+ } | {
3661
+ kind: "REDUCE";
3662
+ reducedQuantity: string;
3663
+ realization: {
3664
+ kind: "PRICED";
3665
+ quantity: string;
3666
+ entryPrice: string;
3667
+ exitPrice: string;
3668
+ grossPnl: `-${string}` | `+${string}`;
3669
+ } | {
3670
+ kind: "UNPRICED";
3671
+ quantity: string;
3672
+ };
3673
+ } | {
3674
+ kind: "CLOSE";
3675
+ reducedQuantity: string;
3676
+ realization: {
3677
+ kind: "PRICED";
3678
+ quantity: string;
3679
+ entryPrice: string;
3680
+ exitPrice: string;
3681
+ grossPnl: `-${string}` | `+${string}`;
3682
+ } | {
3683
+ kind: "UNPRICED";
3684
+ quantity: string;
3685
+ };
3686
+ } | {
3687
+ kind: "FLIP";
3688
+ reducedQuantity: string;
3689
+ openedQuantity: string;
3690
+ realization: {
3691
+ kind: "PRICED";
3692
+ quantity: string;
3693
+ entryPrice: string;
3694
+ exitPrice: string;
3695
+ grossPnl: `-${string}` | `+${string}`;
3696
+ } | {
3697
+ kind: "UNPRICED";
3698
+ quantity: string;
3699
+ };
3700
+ };
3701
+ basisBefore: {
3702
+ signedQuantity: `-${string}` | `+${string}`;
3703
+ basis: {
3704
+ kind: "FLAT";
3705
+ } | {
3706
+ kind: "PRICED";
3707
+ entryPrice: string;
3708
+ } | {
3709
+ kind: "UNPRICED";
3710
+ };
3711
+ };
3712
+ basisAfter: {
3713
+ signedQuantity: `-${string}` | `+${string}`;
3714
+ basis: {
3715
+ kind: "FLAT";
3716
+ } | {
3717
+ kind: "PRICED";
3718
+ entryPrice: string;
3719
+ } | {
3720
+ kind: "UNPRICED";
3721
+ };
3722
+ };
3723
+ tradeFee?: `-${string}` | `+${string}` | undefined;
3724
+ pearFee?: `-${string}` | `+${string}` | undefined;
3725
+ executionId?: string | undefined;
3726
+ }[];
3727
+ executionId?: string | undefined;
3728
+ scheduleId?: string | undefined;
3729
+ triggerId?: string | undefined;
3730
+ }[];
3731
+ nextCursor?: string | undefined;
3732
+ }>;
3550
3733
  };
3551
3734
  portfolio: {
3552
3735
  overview: (query?: HttpTypes.PortfolioQueryInput, headers?: ClientHeaders) => Promise<{
3553
- intervals: {
3554
- oneDay: {
3555
- periodStart: string;
3556
- periodEnd: string;
3557
- volume: number;
3558
- openInterest: number;
3559
- winningTradesCount: number;
3560
- winningTradesUsd: number;
3561
- losingTradesCount: number;
3562
- losingTradesUsd: number;
3563
- }[];
3564
- oneWeek: {
3565
- periodStart: string;
3566
- periodEnd: string;
3567
- volume: number;
3568
- openInterest: number;
3569
- winningTradesCount: number;
3570
- winningTradesUsd: number;
3571
- losingTradesCount: number;
3572
- losingTradesUsd: number;
3573
- }[];
3574
- oneMonth: {
3575
- periodStart: string;
3576
- periodEnd: string;
3577
- volume: number;
3578
- openInterest: number;
3579
- winningTradesCount: number;
3580
- winningTradesUsd: number;
3581
- losingTradesCount: number;
3582
- losingTradesUsd: number;
3583
- }[];
3584
- oneYear: {
3585
- periodStart: string;
3586
- periodEnd: string;
3587
- volume: number;
3588
- openInterest: number;
3589
- winningTradesCount: number;
3590
- winningTradesUsd: number;
3591
- losingTradesCount: number;
3592
- losingTradesUsd: number;
3593
- }[];
3594
- all: {
3595
- periodStart: string;
3596
- periodEnd: string;
3597
- volume: number;
3598
- openInterest: number;
3599
- winningTradesCount: number;
3600
- winningTradesUsd: number;
3601
- losingTradesCount: number;
3602
- losingTradesUsd: number;
3603
- }[];
3604
- } | null;
3605
- overall: {
3606
- totalWinningTradesCount: number;
3607
- totalLosingTradesCount: number;
3608
- totalWinningUsd: number;
3609
- totalLosingUsd: number;
3610
- currentOpenInterest: number;
3611
- currentTotalVolume: number;
3736
+ realizedCashflow: {
3737
+ grossPnl: number;
3738
+ tradeFees: number;
3739
+ pearFees: number;
3740
+ netCashflow: number;
3741
+ funding: number;
3742
+ netCashflowWithFunding: number;
3743
+ undeterminedQuantityByAsset: Record<string, number>;
3744
+ fundingSynced: boolean;
3745
+ intervals?: {
3746
+ oneDay: {
3747
+ grossPnl: number;
3748
+ tradeFees: number;
3749
+ pearFees: number;
3750
+ netCashflow: number;
3751
+ funding: number;
3752
+ netCashflowWithFunding: number;
3753
+ undeterminedQuantityByAsset: Record<string, number>;
3754
+ periodStart: string;
3755
+ periodEnd: string;
3756
+ }[];
3757
+ oneWeek: {
3758
+ grossPnl: number;
3759
+ tradeFees: number;
3760
+ pearFees: number;
3761
+ netCashflow: number;
3762
+ funding: number;
3763
+ netCashflowWithFunding: number;
3764
+ undeterminedQuantityByAsset: Record<string, number>;
3765
+ periodStart: string;
3766
+ periodEnd: string;
3767
+ }[];
3768
+ oneMonth: {
3769
+ grossPnl: number;
3770
+ tradeFees: number;
3771
+ pearFees: number;
3772
+ netCashflow: number;
3773
+ funding: number;
3774
+ netCashflowWithFunding: number;
3775
+ undeterminedQuantityByAsset: Record<string, number>;
3776
+ periodStart: string;
3777
+ periodEnd: string;
3778
+ }[];
3779
+ oneYear: {
3780
+ grossPnl: number;
3781
+ tradeFees: number;
3782
+ pearFees: number;
3783
+ netCashflow: number;
3784
+ funding: number;
3785
+ netCashflowWithFunding: number;
3786
+ undeterminedQuantityByAsset: Record<string, number>;
3787
+ periodStart: string;
3788
+ periodEnd: string;
3789
+ }[];
3790
+ all: {
3791
+ grossPnl: number;
3792
+ tradeFees: number;
3793
+ pearFees: number;
3794
+ netCashflow: number;
3795
+ funding: number;
3796
+ netCashflowWithFunding: number;
3797
+ undeterminedQuantityByAsset: Record<string, number>;
3798
+ periodStart: string;
3799
+ periodEnd: string;
3800
+ }[];
3801
+ } | undefined;
3802
+ };
3803
+ closedPositionStats: {
3804
+ totalPositions: number;
3805
+ wins: number;
3806
+ losses: number;
3807
+ hitRate: number | null;
3808
+ profitFactor: number | null;
3809
+ avgWinSize: number | null;
3810
+ avgLossSize: number | null;
3811
+ netPnl: number;
3812
+ positionPnls: number[];
3813
+ positionReturns: number[];
3814
+ };
3815
+ openSnapshot: {
3816
+ openInterest: number;
3612
3817
  unrealizedPnl: number;
3613
- realizedPnl: number;
3614
- totalFees: number;
3615
- totalTrades: number;
3818
+ };
3819
+ volume: {
3820
+ total: number;
3821
+ intervals?: {
3822
+ oneDay: {
3823
+ periodStart: string;
3824
+ periodEnd: string;
3825
+ volume: number;
3826
+ }[];
3827
+ oneWeek: {
3828
+ periodStart: string;
3829
+ periodEnd: string;
3830
+ volume: number;
3831
+ }[];
3832
+ oneMonth: {
3833
+ periodStart: string;
3834
+ periodEnd: string;
3835
+ volume: number;
3836
+ }[];
3837
+ oneYear: {
3838
+ periodStart: string;
3839
+ periodEnd: string;
3840
+ volume: number;
3841
+ }[];
3842
+ all: {
3843
+ periodStart: string;
3844
+ periodEnd: string;
3845
+ volume: number;
3846
+ }[];
3847
+ } | undefined;
3616
3848
  };
3617
3849
  }>;
3618
3850
  analytics: (query?: HttpTypes.PortfolioAnalyticsQueryInput, headers?: ClientHeaders) => Promise<{
3619
3851
  config: {
3620
- rollingWindowDays: number;
3621
3852
  startDate: string;
3622
3853
  endDate: string;
3854
+ };
3855
+ realizedCashflow: {
3856
+ grossPnl: number;
3857
+ tradeFees: number;
3858
+ pearFees: number;
3859
+ netCashflow: number;
3860
+ funding: number;
3861
+ netCashflowWithFunding: number;
3862
+ undeterminedQuantityByAsset: Record<string, number>;
3623
3863
  fundingSynced: boolean;
3864
+ series?: {
3865
+ grossPnl: number;
3866
+ tradeFees: number;
3867
+ pearFees: number;
3868
+ netCashflow: number;
3869
+ funding: number;
3870
+ netCashflowWithFunding: number;
3871
+ undeterminedQuantityByAsset: Record<string, number>;
3872
+ periodStart: string;
3873
+ periodEnd: string;
3874
+ }[] | undefined;
3624
3875
  };
3625
- portfolio: {
3626
- realizedOnly: {
3627
- pnlAbsolute: number;
3628
- pnlPercent: number | null;
3629
- hitRate: number | null;
3630
- profitFactor: number | null;
3631
- avgWinSize: number | null;
3632
- avgLossSize: number | null;
3633
- avgReturnPerWin: number | null;
3634
- avgReturnPerLoss: number | null;
3635
- maxDrawdown: number | null;
3636
- sharpeRatio: number | null;
3637
- sortinoRatio: number | null;
3638
- };
3639
- realizedPlusUnrealized: {
3640
- pnlAbsolute: number;
3641
- pnlPercent: number | null;
3642
- hitRate: number | null;
3643
- profitFactor: number | null;
3644
- avgWinSize: number | null;
3645
- avgLossSize: number | null;
3646
- avgReturnPerWin: number | null;
3647
- avgReturnPerLoss: number | null;
3648
- maxDrawdown: number | null;
3649
- sharpeRatio: number | null;
3650
- sortinoRatio: number | null;
3651
- };
3652
- realizedHitRate: number | null;
3876
+ closedPositionStats: {
3877
+ totalPositions: number;
3878
+ wins: number;
3879
+ losses: number;
3880
+ hitRate: number | null;
3653
3881
  profitFactor: number | null;
3654
3882
  avgWinSize: number | null;
3655
3883
  avgLossSize: number | null;
3656
- breakevenHitRate: number | null;
3657
- cushion: number | null;
3884
+ netPnl: number;
3885
+ positionPnls: number[];
3886
+ positionReturns: number[];
3658
3887
  };
3659
- assets: {
3660
- asset: string;
3661
- realizedOnly: {
3662
- pnlAbsolute: number;
3663
- pnlPercent: number | null;
3664
- hitRate: number | null;
3665
- profitFactor: number | null;
3666
- avgWinSize: number | null;
3667
- avgLossSize: number | null;
3668
- avgReturnPerWin: number | null;
3669
- avgReturnPerLoss: number | null;
3670
- maxDrawdown: number | null;
3671
- sharpeRatio: number | null;
3672
- sortinoRatio: number | null;
3673
- };
3674
- realizedPlusUnrealized: {
3675
- pnlAbsolute: number;
3676
- pnlPercent: number | null;
3677
- hitRate: number | null;
3678
- profitFactor: number | null;
3679
- avgWinSize: number | null;
3680
- avgLossSize: number | null;
3681
- avgReturnPerWin: number | null;
3682
- avgReturnPerLoss: number | null;
3683
- maxDrawdown: number | null;
3684
- sharpeRatio: number | null;
3685
- sortinoRatio: number | null;
3686
- };
3687
- unrealizedReturnPerDay: number | null;
3688
- }[];
3689
- pairs: {
3888
+ pairBreakdown: {
3690
3889
  key: string;
3691
3890
  longAssets: string[];
3692
3891
  shortAssets: string[];
3693
- trades: number;
3892
+ positions: number;
3694
3893
  wins: number;
3695
3894
  losses: number;
3696
3895
  hitRate: number | null;
3697
- avgWinSize: number | null;
3698
- avgLossSize: number | null;
3699
- expectancy: number | null;
3700
- totalPnl: number;
3896
+ profitFactor: number | null;
3897
+ expectancy: number;
3898
+ grossPnl: number;
3899
+ tradeFees: number;
3900
+ pearFees: number;
3901
+ netPnl: number;
3701
3902
  bookPercent: number | null;
3702
3903
  }[];
3703
- series?: {
3704
- date: string;
3705
- trades: number;
3706
- wins: number;
3707
- losses: number;
3708
- dayPnl: number;
3709
- dayVolume: number;
3710
- dayFunding: number;
3711
- dayNetPnl: number;
3712
- hitRate: number | null;
3713
- winLossRatio: number | null;
3714
- rollingHitRate: number | null;
3715
- rollingWinLossRatio: number | null;
3716
- dailyReturn: number | null;
3717
- cumulativeReturn: number;
3718
- equity: number;
3719
- drawdown: number;
3720
- rollingSharpe: number | null;
3721
- rollingSortino: number | null;
3722
- rollingVolatility: number | null;
3723
- }[] | undefined;
3904
+ openSnapshot: {
3905
+ openInterest: number;
3906
+ unrealizedPnl: number;
3907
+ };
3908
+ volume: {
3909
+ total: number;
3910
+ series?: {
3911
+ periodStart: string;
3912
+ periodEnd: string;
3913
+ volume: number;
3914
+ }[] | undefined;
3915
+ };
3724
3916
  }>;
3725
3917
  };
3726
3918
  prices: {
@@ -3917,9 +4109,11 @@ declare class Core {
3917
4109
  limit: number;
3918
4110
  count: number;
3919
4111
  pendingCount: number;
4112
+ totalVolumeUsd: string;
3920
4113
  referees: ({
3921
4114
  status: "BOUND";
3922
4115
  tradeCount: number;
4116
+ volumeUsd: string;
3923
4117
  address?: `0x${string}` | undefined;
3924
4118
  referredAt?: number | undefined;
3925
4119
  } | {
@@ -4168,6 +4362,7 @@ declare class Core {
4168
4362
  createBulk: (body: HttpTypes.CreateFundingPaymentsRequest, headers?: ClientHeaders) => Promise<Record<string, never>>;
4169
4363
  };
4170
4364
  };
4365
+ /** Basket commands; executed-fill history lives under the plural `trades` namespace. */
4171
4366
  trade: {
4172
4367
  open: (body: HttpTypes.CreateTradeOpen, headers?: ClientHeaders) => Promise<{
4173
4368
  execution: {
@@ -309,10 +309,12 @@ class Core {
309
309
  });
310
310
  }, "openLite"),
311
311
  /**
312
- * Closed-trade history, newest close first, composed server-side.
312
+ * Closed-position history, newest close first, composed server-side.
313
313
  *
314
- * A page may hold fewer than `limit` trades and still return a `nextCursor` —
314
+ * A page may hold fewer than `limit` positions and still return a `nextCursor` —
315
315
  * page until `nextCursor` is absent, never until a page comes back short.
316
+ * `includeSteps` is accepted only together with `positionId`: the timeline is
317
+ * unbounded per row, so it is served one position at a time.
316
318
  */
317
319
  closed: /* @__PURE__ */ __name(async (query, headers) => {
318
320
  return await this.client.fetch(`/positions/closed`, {
@@ -320,7 +322,33 @@ class Core {
320
322
  headers,
321
323
  query
322
324
  });
323
- }, "closed")
325
+ }, "closed"),
326
+ /**
327
+ * Open-position history: the realized part of every open position, newest
328
+ * opened first, composed server-side. Every open position yields exactly
329
+ * one row — one whose fills are not visible yet comes back as an explicit
330
+ * zero-state row.
331
+ *
332
+ * Page until `nextCursor` is absent, never until a page comes back short.
333
+ */
334
+ open: /* @__PURE__ */ __name(async (query, headers) => {
335
+ return await this.client.fetch(`/positions/open`, {
336
+ method: "GET",
337
+ headers,
338
+ query
339
+ });
340
+ }, "open")
341
+ };
342
+ /** Fill-level history; basket commands live under the singular `trade` namespace. */
343
+ trades = {
344
+ /** List fill-level trades grouped by execution with compound cursor pagination. */
345
+ list: /* @__PURE__ */ __name(async (query, headers) => {
346
+ return await this.client.fetch(`/trades`, {
347
+ method: "GET",
348
+ headers,
349
+ query
350
+ });
351
+ }, "list")
324
352
  };
325
353
  portfolio = {
326
354
  overview: /* @__PURE__ */ __name(async (query, headers) => {
@@ -560,6 +588,7 @@ class Core {
560
588
  }, "createBulk")
561
589
  }
562
590
  };
591
+ /** Basket commands; executed-fill history lives under the plural `trades` namespace. */
563
592
  trade = {
564
593
  open: /* @__PURE__ */ __name(async (body, headers) => {
565
594
  return await this.client.fetch(`/trade/open`, {
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Auth } from './auth/index.js';
2
2
  import { Core } from './core/index.js';
3
3
  import { External } from './external/index.js';
4
4
  import { SDKConfig } from './types.js';
5
+ export { AuthErrorContext } from './types.js';
5
6
  export { ExternalTrigger, ExternalTriggerSource, HttpTypes, SearchTriggersParams, SearchTriggersResponse, TriggerOutcome } from '@pear-protocol/types';
6
7
  export * from '@pear-protocol/utils';
7
8
  export { FetchError } from './utils.js';
package/dist/index.js CHANGED
@@ -91,13 +91,47 @@ class PearSDK {
91
91
  });
92
92
  if (config.onAuthError) {
93
93
  const onAuthError = config.onAuthError;
94
- client.useResponse(async (response) => {
95
- if (response.status === 401) {
96
- onAuthError();
94
+ const replayed = /* @__PURE__ */ new WeakSet();
95
+ const recoveries = /* @__PURE__ */ new Map();
96
+ const recover = /* @__PURE__ */ __name((request, failedCredential) => {
97
+ const inFlight = recoveries.get(failedCredential);
98
+ if (inFlight) {
99
+ logger.debug("[bearer-auth] Recovery already in flight for this credential, waiting...");
100
+ return inFlight;
101
+ }
102
+ const context = {
103
+ request: request.clone(),
104
+ failedCredential
105
+ };
106
+ const recovery = (async () => onAuthError(context))().finally(() => {
107
+ if (recoveries.get(failedCredential) === recovery) {
108
+ recoveries.delete(failedCredential);
109
+ }
110
+ });
111
+ recoveries.set(failedCredential, recovery);
112
+ return recovery;
113
+ }, "recover");
114
+ const bearerAuthMiddleware = /* @__PURE__ */ __name(async (response, retry, request) => {
115
+ if (response.status !== 401) {
116
+ return response;
117
+ }
118
+ if (replayed.has(request)) {
119
+ logger.error("[bearer-auth] Replay after recovery returned 401 again, giving up");
97
120
  throw new FetchError("Unauthorized. Token may be expired or revoked.", "Unauthorized", 401);
98
121
  }
99
- return response;
100
- });
122
+ const failedCredential = readBearerCredential(request);
123
+ logger.warn(`[bearer-auth] 401 received for ${request.method} ${request.url}`);
124
+ replayed.add(request);
125
+ try {
126
+ await recover(request, failedCredential);
127
+ } catch (err) {
128
+ logger.error("[bearer-auth] Recovery rejected, not replaying:", String(err));
129
+ throw new FetchError("Unauthorized. Token may be expired or revoked.", "Unauthorized", 401);
130
+ }
131
+ logger.info(`[bearer-auth] Replaying ${request.method} ${request.url}`);
132
+ return retry();
133
+ }, "bearerAuthMiddleware");
134
+ client.useResponse(bearerAuthMiddleware);
101
135
  }
102
136
  }
103
137
  if (authMode === "apikey" && config.auth?.key) {
@@ -111,13 +145,20 @@ class PearSDK {
111
145
  });
112
146
  if (config.onAuthError) {
113
147
  const onAuthError = config.onAuthError;
114
- client.useResponse(async (response) => {
115
- if (response.status === 401) {
116
- onAuthError();
117
- throw new FetchError("Unauthorized. API key may be expired or revoked.", "Unauthorized", 401);
148
+ const apiKeyAuthMiddleware = /* @__PURE__ */ __name(async (response, _retry, request) => {
149
+ if (response.status !== 401) {
150
+ return response;
118
151
  }
119
- return response;
120
- });
152
+ const notified = onAuthError({
153
+ request: request.clone(),
154
+ failedCredential: request.headers.get("x-api-key")
155
+ });
156
+ void Promise.resolve(notified).catch((err) => {
157
+ logger.error("[apikey-auth] onAuthError callback rejected:", String(err));
158
+ });
159
+ throw new FetchError("Unauthorized. API key may be expired or revoked.", "Unauthorized", 401);
160
+ }, "apiKeyAuthMiddleware");
161
+ client.useResponse(apiKeyAuthMiddleware);
121
162
  }
122
163
  }
123
164
  if (config.getTradeAccountId) {
@@ -142,5 +183,13 @@ function isSessionRefreshRequest(url) {
142
183
  return pathname.endsWith("/auth/session/refresh") || pathname.endsWith("/auth/session/refresh/token");
143
184
  }
144
185
  __name(isSessionRefreshRequest, "isSessionRefreshRequest");
186
+ function readBearerCredential(request) {
187
+ const header = request.headers.get("authorization");
188
+ if (!header) {
189
+ return null;
190
+ }
191
+ return header.replace(/^Bearer\s+/i, "");
192
+ }
193
+ __name(readBearerCredential, "readBearerCredential");
145
194
 
146
195
  export { PearSDK as default };
package/dist/types.d.ts CHANGED
@@ -2,6 +2,20 @@ import { HttpTypes } from '@pear-protocol/types';
2
2
  import { ProxyConfig } from '@pear-protocol/utils';
3
3
 
4
4
  type AuthMode = 'session' | 'bearer' | 'apikey';
5
+ /** What received a 401, so `onAuthError` can tell whose request it was. */
6
+ type AuthErrorContext = {
7
+ /**
8
+ * A snapshot clone of the request that received the 401 — same method, URL, headers and
9
+ * body, but safe to consume: reading it never disturbs the underlying request, in
10
+ * bearer mode the very request the SDK is about to replay.
11
+ */
12
+ request: Request;
13
+ /**
14
+ * The credential that request carried: the bearer token without its `Bearer ` prefix, or
15
+ * the `x-api-key` value. `null` when the getter supplied none.
16
+ */
17
+ failedCredential: string | null;
18
+ };
5
19
  type SDKConfig = {
6
20
  /**
7
21
  * API endpoint.
@@ -58,14 +72,21 @@ type SDKConfig = {
58
72
  */
59
73
  clientId?: string | null;
60
74
  /**
61
- * Called when server returns 401 (bearer / apikey mode).
75
+ * Called when the server returns 401 (bearer / apikey mode). The context carries the
76
+ * request and credential that actually received the 401, not the getter's current value.
77
+ *
78
+ * Bearer mode awaits the callback, then replays the original request exactly once with
79
+ * the token the getter returns afterward; reject to refuse the replay (e.g. after an
80
+ * identity switch). A second 401 ends the request without calling back. Refresh through
81
+ * a separate transport — calling this SDK instance from the callback deadlocks. Apikey
82
+ * mode stays notify-only: no await, no replay.
62
83
  *
63
- * Example: () => {
64
- * console.error('Unauthorized. Token may be expired or revoked.');
65
- * localStorage.removeItem('apiKey');
84
+ * Example: async ({ failedCredential }) => {
85
+ * if (failedCredential !== activeToken()) throw new Error('identity changed');
86
+ * await refreshTokenViaSeparateTransport();
66
87
  * }
67
88
  */
68
- onAuthError?: () => void;
89
+ onAuthError?: (context: AuthErrorContext) => void | Promise<void>;
69
90
  /**
70
91
  * Called when the session cannot be recovered (session mode only).
71
92
  * Fires after a 401 when the subsequent `POST /auth/refresh` also fails.
@@ -132,4 +153,4 @@ type RequestMiddleware = (request: Request, next: () => Promise<Response>) => Pr
132
153
  type ResponseMiddleware = (response: Response, retry: () => Promise<Response>, request: Request) => Promise<Response>;
133
154
  type ClientIdBody = HttpTypes.RebalanceManual | HttpTypes.CreateSchedule | HttpTypes.CreateTradeOpen | HttpTypes.CreateTradeAdjust | HttpTypes.CreateTradeClose | HttpTypes.CreateTradeCloseAll | HttpTypes.CreateTriggerOpen | HttpTypes.CreateTriggerClose;
134
155
 
135
- export type { AuthMode, ClientHeaders, ClientIdBody, FetchArgs, FetchInput, FetchParams, Logger, RequestMiddleware, ResponseMiddleware, SDKConfig };
156
+ export type { AuthErrorContext, AuthMode, ClientHeaders, ClientIdBody, FetchArgs, FetchInput, FetchParams, Logger, RequestMiddleware, ResponseMiddleware, SDKConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/core-sdk",
3
- "version": "1.20.0",
3
+ "version": "2.0.0",
4
4
  "description": "Pear Protocol Core SDK",
5
5
  "private": false,
6
6
  "type": "module",
@@ -26,8 +26,8 @@
26
26
  "typecheck": "tsc --noEmit"
27
27
  },
28
28
  "dependencies": {
29
- "@pear-protocol/types": "^1.32.0",
30
- "@pear-protocol/utils": "^0.8.1",
29
+ "@pear-protocol/types": "^2.0.0",
30
+ "@pear-protocol/utils": "^1.0.0",
31
31
  "qs": "^6.14.0"
32
32
  },
33
33
  "devDependencies": {