@fanfare-io/fanfare-sdk-core 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appointments/appointment.module.d.ts +7 -6
- package/dist/appointments/appointment.module.js +1 -1
- package/dist/appointments/types.d.ts +12 -6
- package/dist/auctions/auction-status.d.ts +4 -0
- package/dist/auctions/auction-status.js +1 -0
- package/dist/auctions/auction.driver.d.ts +14 -2
- package/dist/auctions/auction.driver.js +1 -1
- package/dist/auctions/auction.module.d.ts +1 -0
- package/dist/auctions/auction.module.js +1 -1
- package/dist/auctions/auction.sequence.d.ts +2 -0
- package/dist/auctions/auction.sequence.js +1 -0
- package/dist/auctions/public.d.ts +1 -1
- package/dist/auctions/types.d.ts +6 -9
- package/dist/auth/auth.module.d.ts +8 -0
- package/dist/auth/auth.module.js +1 -1
- package/dist/auth/types.d.ts +15 -0
- package/dist/beacon/beacon.module.d.ts +14 -2
- package/dist/beacon/beacon.module.js +1 -1
- package/dist/beacon/index.d.ts +1 -1
- package/dist/beacon/public.d.ts +1 -1
- package/dist/beacon/types.d.ts +66 -0
- package/dist/core/client.js +2 -2
- package/dist/core/error-display.js +1 -1
- package/dist/core/error-disposition.js +1 -1
- package/dist/core/errors.d.ts +34 -1
- package/dist/core/errors.js +1 -1
- package/dist/core/http.js +1 -1
- package/dist/draws/draw.driver.d.ts +2 -2
- package/dist/draws/draw.driver.js +1 -1
- package/dist/draws/draw.module.d.ts +8 -3
- package/dist/draws/draw.module.js +1 -1
- package/dist/draws/draw.sequence.d.ts +216 -12
- package/dist/draws/draw.sequence.js +1 -0
- package/dist/draws/public.d.ts +1 -1
- package/dist/draws/types.d.ts +34 -3
- package/dist/experiences/distribution-monitor.types.d.ts +32 -7
- package/dist/experiences/dom-bridge.d.ts +40 -0
- package/dist/experiences/dom-bridge.js +1 -0
- package/dist/experiences/experience.module.d.ts +6 -5
- package/dist/experiences/experience.module.js +1 -1
- package/dist/experiences/journey-contract.fixtures.d.ts +3 -0
- package/dist/experiences/journey-display.d.ts +59 -0
- package/dist/experiences/journey-display.js +1 -0
- package/dist/experiences/journey-view.d.ts +5 -0
- package/dist/experiences/journey-view.js +1 -1
- package/dist/experiences/journey.d.ts +83 -1
- package/dist/experiences/journey.js +1 -1
- package/dist/experiences/journey.machine.d.ts +11 -0
- package/dist/experiences/journey.machine.js +1 -1
- package/dist/experiences/journey.types.d.ts +11 -45
- package/dist/experiences/public.d.ts +15 -5
- package/dist/experiences/public.js +1 -1
- package/dist/experiences/sequence-driver.d.ts +31 -4
- package/dist/experiences/settlement.d.ts +3 -0
- package/dist/experiences/settlement.js +1 -0
- package/dist/experiences/types.d.ts +13 -4
- package/dist/internals.d.ts +5 -1
- package/dist/internals.js +1 -1
- package/dist/payment/checkout-contract.d.ts +39 -0
- package/dist/payment/checkout-contract.js +1 -0
- package/dist/payment/payment.module.d.ts +16 -0
- package/dist/payment/payment.module.js +1 -0
- package/dist/payment/reservations.module.d.ts +17 -0
- package/dist/payment/reservations.module.js +1 -0
- package/dist/payment/types.d.ts +68 -0
- package/dist/queues/queue.driver.js +1 -1
- package/dist/queues/queue.module.d.ts +2 -1
- package/dist/queues/queue.module.js +1 -1
- package/dist/queues/queue.sequence.d.ts +214 -10
- package/dist/queues/types.d.ts +48 -13
- package/dist/ssr/ssr-sdk.d.ts +18 -0
- package/dist/ssr/ssr-sdk.js +1 -0
- package/dist/state/capability-token-registry.d.ts +1 -0
- package/dist/state/capability-token-registry.js +1 -1
- package/dist/state/events.d.ts +5 -19
- package/dist/state/store.d.ts +6 -1
- package/dist/state/store.js +1 -1
- package/dist/test-utils/harness-scenarios.d.ts +20 -5
- package/dist/test-utils/harness-scenarios.js +1 -1
- package/dist/test-utils/index.d.ts +1 -0
- package/dist/test-utils/index.js +1 -1
- package/dist/test-utils/mock-journey.d.ts +2 -0
- package/dist/test-utils/mock-journey.js +1 -1
- package/dist/test-utils/mock-sdk-vitest.d.ts +30 -29
- package/dist/test-utils/mock-sdk-vitest.js +1 -1
- package/dist/test-utils/mock-sdk.d.ts +8 -0
- package/dist/test-utils/mock-sdk.js +1 -1
- package/dist/test-utils/mock-server.d.ts +20 -0
- package/dist/test-utils/mock-server.js +1 -1
- package/dist/test-utils/phone-formatter-contract.d.ts +24 -0
- package/dist/test-utils/phone-formatter-contract.js +1 -0
- package/dist/test-utils/verification-scenarios.js +1 -1
- package/dist/timed-releases/timed-release.module.js +1 -1
- package/dist/types/index.d.ts +12 -2
- package/dist/utils/fingerprint.module.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/waitlists/types.d.ts +3 -6
- package/dist/waitlists/waitlist.module.d.ts +3 -3
- package/package.json +16 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpClient } from '../core/http';
|
|
1
|
+
import { HttpClient, RequestOptions } from '../core/http';
|
|
2
2
|
import { MonitorUpdate } from '../experiences/distribution-monitor.types';
|
|
3
3
|
import { AppointmentDetails } from '../experiences/types';
|
|
4
4
|
import { AppointmentBooking, AppointmentModule, AppointmentSlot } from './types';
|
|
@@ -8,21 +8,22 @@ export declare class AppointmentManagementModule implements AppointmentModule {
|
|
|
8
8
|
private readonly monitorRuntime;
|
|
9
9
|
private readonly pollSchedulers;
|
|
10
10
|
private readonly monitorGenerations;
|
|
11
|
+
private readonly observedBooking;
|
|
11
12
|
private readonly pollingIntervalMs;
|
|
12
13
|
private destroyed;
|
|
13
14
|
constructor(http: HttpClient, opts?: {
|
|
14
15
|
pollingIntervalMs?: number;
|
|
15
16
|
});
|
|
16
|
-
get(appointmentId: string): Promise<AppointmentDetails>;
|
|
17
|
+
get(appointmentId: string, options?: RequestOptions): Promise<AppointmentDetails>;
|
|
17
18
|
getSlots(appointmentId: string, range: {
|
|
18
19
|
startDate: string;
|
|
19
20
|
endDate: string;
|
|
20
21
|
timezone?: string;
|
|
21
|
-
}): Promise<AppointmentSlot[]>;
|
|
22
|
+
}, options?: RequestOptions): Promise<AppointmentSlot[]>;
|
|
22
23
|
getMe(appointmentId: string): Promise<AppointmentBooking | null>;
|
|
23
|
-
book(appointmentId: string, slotId: string, locationId?: string): Promise<AppointmentBooking>;
|
|
24
|
-
cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string): Promise<void>;
|
|
25
|
-
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string): Promise<AppointmentBooking>;
|
|
24
|
+
book(appointmentId: string, slotId: string, locationId?: string, options?: RequestOptions): Promise<AppointmentBooking>;
|
|
25
|
+
cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string, options?: RequestOptions): Promise<void>;
|
|
26
|
+
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string, options?: RequestOptions): Promise<AppointmentBooking>;
|
|
26
27
|
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
|
|
27
28
|
stopMonitoring(id: string): void;
|
|
28
29
|
isMonitoring(id: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as t from"valibot";import{isFanfareError as o,createError as i}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{parseResponse as e}from"../core/parse-response.js";import{createPollScheduler as r}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as s}from"../experiences/distribution-monitor.runtime.js";function a(t,o,i){const n=e(t,o,{endpoint:i},{throwOnFailure:!0});if(!n.ok)throw n.error;return n.value}const l=t.object({id:t.string(),startTime:t.string(),endTime:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),timezone:t.nullish(t.string()),capacity:t.number(),available:t.number()}),d=t.object({slots:t.array(l)}),m=t.object({id:t.string(),type:t.literal("appointment"),openAt:t.nullish(t.string()),closeAt:t.nullish(t.string()),slotDurationMinutes:t.nullish(t.number()),timeZone:t.nullish(t.string()),locationId:t.nullish(t.string()),totalSlots:t.nullish(t.number()),availableSlots:t.nullish(t.number())}),c=t.picklist(["booked","checked_in","completed"]),p=t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.string(),status:c,createdAt:t.string()}),h=t.object({booking:t.nullish(t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),joinUrl:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.nullish(t.string()),status:c}))}),g=t.object({bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:c});function u(t){return{id:t.id,startTime:t.startTime,endTime:t.endTime,locationId:t.locationId??void 0,locationName:t.locationName??void 0,timezone:t.timezone??void 0,capacity:t.capacity,available:t.available}}class I{constructor(t,o){this.logger=n(),this.monitorRuntime=new s,this.pollSchedulers=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.destroyed=!1,this.http=t,this.pollingIntervalMs=o?.pollingIntervalMs??3e4}async get(t){try{const o=await this.http.get(`/appointments/${t}
|
|
1
|
+
import*as t from"valibot";import{isFanfareError as o,createError as i}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{parseResponse as e}from"../core/parse-response.js";import{createPollScheduler as r}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as s}from"../experiences/distribution-monitor.runtime.js";function a(t,o,i){const n=e(t,o,{endpoint:i},{throwOnFailure:!0});if(!n.ok)throw n.error;return n.value}const l=t.object({id:t.string(),startTime:t.string(),endTime:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),timezone:t.nullish(t.string()),capacity:t.number(),available:t.number()}),d=t.object({slots:t.array(l)}),m=t.object({id:t.string(),type:t.literal("appointment"),openAt:t.nullish(t.string()),closeAt:t.nullish(t.string()),slotDurationMinutes:t.nullish(t.number()),timeZone:t.nullish(t.string()),locationId:t.nullish(t.string()),totalSlots:t.nullish(t.number()),availableSlots:t.nullish(t.number())}),c=t.picklist(["booked","checked_in","completed","cancelled","no_show"]),p=t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.string(),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),startTime:t.string(),endTime:t.string(),confirmationCode:t.string(),status:c,createdAt:t.string()}),h=t.object({booking:t.nullish(t.object({bookingId:t.string(),appointmentId:t.string(),consumerId:t.string(),slotId:t.nullish(t.string()),locationId:t.nullish(t.string()),locationName:t.nullish(t.string()),joinUrl:t.nullish(t.string()),startTime:t.nullish(t.string()),endTime:t.nullish(t.string()),confirmationCode:t.nullish(t.string()),status:c}))}),g=t.object({bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:c});function u(t){return{id:t.id,startTime:t.startTime,endTime:t.endTime,locationId:t.locationId??void 0,locationName:t.locationName??void 0,timezone:t.timezone??void 0,capacity:t.capacity,available:t.available}}class I{constructor(t,o){this.logger=n(),this.monitorRuntime=new s,this.pollSchedulers=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.observedBooking=/* @__PURE__ */new Map,this.destroyed=!1,this.http=t,this.pollingIntervalMs=o?.pollingIntervalMs??3e4}async get(t,n){try{const o=await this.http.get(`/appointments/${t}`,n),i=a(m,o,`/appointments/${t}`);return{id:i.id,type:"appointment",openAt:i.openAt??void 0,closeAt:i.closeAt??void 0,slotDurationMinutes:i.slotDurationMinutes??void 0,bookingWindowOpenAt:i.openAt??void 0,bookingWindowCloseAt:i.closeAt??void 0,timezone:i.timeZone??void 0,locationId:i.locationId??void 0,totalSlots:i.totalSlots??void 0,availableSlots:i.availableSlots??void 0}}catch(e){if(this.logger.error("Failed to get appointment",{appointmentId:t,error:e}),o(e))throw e;throw i.networkError("Failed to get appointment",{error:e})}}async getSlots(t,n,e){try{const o=new URLSearchParams({startDate:n.startDate,endDate:n.endDate});n.timezone&&o.set("timezone",n.timezone);const i=await this.http.get(`/appointments/${t}/slots?${o.toString()}`,e);return a(d,i,`/appointments/${t}/slots`).slots.map(u)}catch(r){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:r}),o(r))throw r;throw i.networkError("Failed to get appointment slots",{error:r})}}async getMe(t){try{const o=await this.http.get(`/appointments/${t}/me`),i=a(h,o,`/appointments/${t}/me`);return i.booking?function(t){return{bookingId:t.bookingId,appointmentId:t.appointmentId,slotId:t.slotId??"",locationId:t.locationId??void 0,locationName:t.locationName??void 0,joinUrl:t.joinUrl??void 0,startTime:t.startTime??"",endTime:t.endTime??"",confirmationCode:t.confirmationCode??void 0,consumerStatus:t.status}}(i.booking):null}catch(n){if(this.logger.error("Failed to get current booking",{appointmentId:t,error:n}),o(n))throw n;throw i.networkError("Failed to get current booking",{error:n})}}async book(t,n,e,r){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:n,locationId:e??null},r);return function(t){return{bookingId:t.bookingId,appointmentId:t.appointmentId,slotId:t.slotId,locationId:t.locationId??void 0,locationName:t.locationName??void 0,startTime:t.startTime,endTime:t.endTime,confirmationCode:t.confirmationCode,consumerStatus:t.status}}(a(p,o,`/appointments/${t}/book`))}catch(s){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:n,locationId:e,error:s}),o(s))throw s;throw i.networkError("Failed to book appointment",{error:s})}}async cancel(t,n,e,r,s){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:n,locationId:e??null,reason:r??null},s)}catch(a){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:n,locationId:e,error:a}),o(a))throw a;throw i.networkError("Failed to cancel appointment",{error:a})}}async reschedule(t,n,e,r){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:n,newLocationId:e??null},r),i=a(g,o,`/appointments/${t}/reschedule`),s=await this.getMe(t);return s?{...s,confirmationCode:i.confirmationCode}:{bookingId:i.bookingId,appointmentId:t,slotId:i.newSlotId,locationId:e,startTime:"",endTime:"",confirmationCode:i.confirmationCode,consumerStatus:i.status}}catch(s){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:n,newLocationId:e,error:s}),o(s))throw s;throw i.networkError("Failed to reschedule appointment",{error:s})}}startMonitoring(t,o,i,n){if(this.destroyed)return;this.monitorRuntime.start(t,o,i),this.stopPolling(t);const e=this.monitorRuntime.getDisplayAtom(t)?.get();this.observedBooking.set(t,"appointment"===e?.type&&void 0!==e.booking);const s=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,s);const a=r({baseMs:this.pollingIntervalMs,suppressBackoff:()=>!1,onDegradedChange:n,shouldContinue:()=>!this.destroyed&&this.monitorGenerations.get(t)===s&&this.pollSchedulers.has(t),run:async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===s)try{const o=await this.getMe(t);if(this.destroyed||this.monitorGenerations.get(t)!==s)return;const i=this.monitorRuntime.getDisplayAtom(t);if(!i)return;if(!o)return void(this.observedBooking.get(t)&&(this.monitorRuntime.notify(t,{type:"ended",outcome:{type:"closed"}}),this.stopMonitoring(t)));if(this.observedBooking.set(t,!0),"cancelled"===o.consumerStatus||"no_show"===o.consumerStatus)return this.monitorRuntime.notify(t,{type:"ended",outcome:{type:o.consumerStatus}}),void this.stopMonitoring(t);const n=Math.max(0,Date.parse(o.startTime)-Date.now());i.set({type:"appointment",booking:{slotId:o.slotId,locationId:o.locationId,locationName:o.locationName,joinUrl:o.joinUrl,startTime:o.startTime,endTime:o.endTime,confirmationCode:o.confirmationCode,consumerStatus:o.consumerStatus},msUntilStart:n})}catch(o){throw this.logger.error("Appointment polling tick failed",{id:t,error:o}),o}}});this.pollSchedulers.set(t,a),a.start()}stopMonitoring(t){this.stopPolling(t),this.monitorRuntime.stop(t),this.observedBooking.delete(t),this.monitorGenerations.set(t,(this.monitorGenerations.get(t)??0)+1)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollSchedulers.has(t)}bumpMonitorGeneration(t){this.monitorGenerations.set(t,(this.monitorGenerations.get(t)??0)+1)}destroy(){this.destroyed=!0,this.monitorGenerations.clear(),this.observedBooking.clear();for(const t of Array.from(this.pollSchedulers.keys()))this.stopPolling(t);this.monitorRuntime.clear()}stopPolling(t){const o=this.pollSchedulers.get(t);void 0!==o&&(o.stop(),this.pollSchedulers.delete(t))}}export{I as AppointmentManagementModule};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { RequestOptions } from '../core/http';
|
|
1
2
|
import { MonitorUpdate } from '../experiences/distribution-monitor.types';
|
|
3
|
+
import { AppointmentDetails } from '../experiences/types';
|
|
2
4
|
export interface AppointmentSlot {
|
|
3
5
|
id: string;
|
|
4
6
|
startTime: string;
|
|
@@ -20,19 +22,23 @@ export interface AppointmentBooking {
|
|
|
20
22
|
startTime: string;
|
|
21
23
|
endTime: string;
|
|
22
24
|
confirmationCode?: string;
|
|
23
|
-
consumerStatus: "booked" | "checked_in" | "completed";
|
|
25
|
+
consumerStatus: "booked" | "checked_in" | "completed" | "cancelled" | "no_show";
|
|
24
26
|
}
|
|
25
27
|
export interface AppointmentModule {
|
|
26
|
-
get(appointmentId: string): Promise<
|
|
28
|
+
get(appointmentId: string, options?: RequestOptions): Promise<AppointmentDetails>;
|
|
27
29
|
getSlots(appointmentId: string, range: {
|
|
28
30
|
startDate: string;
|
|
29
31
|
endDate: string;
|
|
30
32
|
timezone?: string;
|
|
31
|
-
}): Promise<AppointmentSlot[]>;
|
|
33
|
+
}, options?: RequestOptions): Promise<AppointmentSlot[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Own-booking-scoped read. Ungated server-side, so it carries no capability
|
|
36
|
+
* grant and takes no request options.
|
|
37
|
+
*/
|
|
32
38
|
getMe(appointmentId: string): Promise<AppointmentBooking | null>;
|
|
33
|
-
book(appointmentId: string, slotId: string, locationId?: string): Promise<AppointmentBooking>;
|
|
34
|
-
cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string): Promise<void>;
|
|
35
|
-
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string): Promise<AppointmentBooking>;
|
|
39
|
+
book(appointmentId: string, slotId: string, locationId?: string, options?: RequestOptions): Promise<AppointmentBooking>;
|
|
40
|
+
cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string, options?: RequestOptions): Promise<void>;
|
|
41
|
+
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string, options?: RequestOptions): Promise<AppointmentBooking>;
|
|
36
42
|
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
|
|
37
43
|
stopMonitoring(id: string): void;
|
|
38
44
|
isMonitoring(id: string): boolean;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AuctionBidderStateResponse } from '@fanfare-io/fanfare-sdk-contracts/auction';
|
|
2
|
+
import { AuctionConsumerStatus } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
|
|
3
|
+
export declare function toAuctionConsumerStatus(status: AuctionBidderStateResponse["status"]): AuctionConsumerStatus;
|
|
4
|
+
export declare function isTerminalAuctionStatus(status: AuctionConsumerStatus | undefined): status is "won" | "lost";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function n(n){switch(n){case"NOT_BID":return"not_bid";case"BIDDING":return"bidding";case"WINNING":return"winning";case"OUTBID":return"outbid";case"WON":return"won";case"LOST":return"lost"}}function r(n){return"won"===n||"lost"===n}export{r as isTerminalAuctionStatus,n as toAuctionConsumerStatus};
|
|
@@ -4,6 +4,12 @@ import { DistributionSummary, JourneyEvent, JourneyEventAudience, JourneyEventKi
|
|
|
4
4
|
import { SequenceDriver, SequenceProjectionFacts } from '../experiences/sequence-driver';
|
|
5
5
|
import { AuctionModule } from './types';
|
|
6
6
|
type AuctionMonitorModule = AuctionModule & Pick<DistributionMonitor, "startMonitoring" | "stopMonitoring">;
|
|
7
|
+
/**
|
|
8
|
+
* True for an outcome that carries no specific reason (or is absent entirely) and can
|
|
9
|
+
* still be replaced once a more specific terminal result — e.g. floor-unsold — becomes
|
|
10
|
+
* available. A `lost`/`closed` outcome that already has a `reason` is final.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isUpgradeableGenericLoss(outcome: SequenceOutcome | undefined): boolean;
|
|
7
13
|
/**
|
|
8
14
|
* Narrow shell and module capabilities required by the auction driver.
|
|
9
15
|
*/
|
|
@@ -42,7 +48,13 @@ export declare function projectAuctionSequence(facts: SequenceProjectionFacts):
|
|
|
42
48
|
*/
|
|
43
49
|
export declare function withAuctionGrantedConsumer(consumer: MechanismConsumerState | undefined, distribution: DistributionSummary): MechanismConsumerState;
|
|
44
50
|
/**
|
|
45
|
-
* Return the auction consumer state that represents a terminal outcome.
|
|
51
|
+
* Return the auction consumer state that represents a terminal outcome. A not_bid
|
|
52
|
+
* consumer facing a still-upgradeable generic loss is left unresolved (unless
|
|
53
|
+
* `finalize` is set) so a later refresh's floor-unsold re-derivation can still see
|
|
54
|
+
* `not_bid` and upgrade the outcome — finalize=true is for a definitive terminal
|
|
55
|
+
* transition (e.g. an immediate bid result) that will never be re-derived.
|
|
46
56
|
*/
|
|
47
|
-
export declare function withAuctionTerminalConsumer(consumer: MechanismConsumerState | undefined, outcome?: SequenceOutcome
|
|
57
|
+
export declare function withAuctionTerminalConsumer(consumer: MechanismConsumerState | undefined, outcome?: SequenceOutcome, options?: {
|
|
58
|
+
finalize?: boolean;
|
|
59
|
+
}): MechanismConsumerState | undefined;
|
|
48
60
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createError as i}from"../core/errors.js";import{isMoneyString as
|
|
1
|
+
import{AuctionDistributionResults as t}from"@fanfare-io/fanfare-sdk-contracts";import{createError as i}from"../core/errors.js";import{isMoneyString as e,addMoney as n}from"../core/money.js";import{isUnresolvedAuctionConsumer as s}from"./auction.sequence.js";function o(t){return!t||("lost"===t.type||"closed"===t.type)&&!t.reason}function a(t){return new d(t)}function r(e){const{distribution:n,consumer:a,grant:r,outcome:c}=e;if(!n)return{phase:"unavailable"};const u=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(n),d=a&&"auction"===a.mechanism?function(t){const{mechanism:i,...e}=t;return e}(a):p(n);if(r)return"completed"===c?.type?h(u,d,c):r.expiresAt&&Date.now()>new Date(r.expiresAt).getTime()?"open"===u.lifecycle?{phase:"enterable",mechanism:"auction",distribution:u,consumer:p(u)}:h(u,d,{type:"expired"}):{phase:"granted",mechanism:"auction",distribution:u,consumer:d,grant:r};if("closed"===u.lifecycle){if("not_bid"===d.status&&u.result===t.FLOOR_UNSOLD&&o(c))return h(u,d,{type:"lost",reason:t.FLOOR_UNSOLD});if("auction"===a?.mechanism&&!c)return h(u,d,{type:"lost"})}const m=c??function(t){return"lost"===t.status?{type:"lost"}:void 0}(d);return m?h(u,d,m):"settling"===u.lifecycle?{phase:"settling",mechanism:"auction",distribution:u,consumer:d}:s(d)?{phase:"participating",mechanism:"auction",distribution:u,consumer:d}:"scheduled"===u.lifecycle?{phase:"scheduled",mechanism:"auction",distribution:u}:"open"===u.lifecycle?{phase:"enterable",mechanism:"auction",distribution:u,consumer:d}:h(u,d,{type:"closed"})}function c(t,i){return"auction"===t?.mechanism?{...t,status:"won"}:{mechanism:"auction",status:"won",id:i.id,distributionId:i.id}}function u(t,i,e){return t&&"auction"===t.mechanism&&i?"won"===i.type?{...t,status:"won"}:!e?.finalize&&"not_bid"===t.status&&o(i)?t:{...t,status:"lost"}:t}class d{constructor(t){this.deps=t,this.mechanism="auction"}project(t){return r(t)}buildView(t,e){if("unavailable"===t.phase)return{phase:"unavailable",reason:t.reason};const n=function(t){if(!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("Auction driver received a non-auction sequence");return t}(t);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"auction",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt};case"enterable":return{phase:"enterable",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,bid:t=>this.bid(t)};case"participating":return{phase:"participating",mechanism:"auction",consumer:n.consumer,display$:e.displayAtom,bid:t=>this.bid(t)};case"settling":return{phase:"settling",mechanism:"auction",distribution:n.distribution,consumer:n.consumer};case"granted":return{phase:"granted",mechanism:"auction",distribution:n.distribution,consumer:n.consumer,grant:n.grant,claim:()=>e.claim()};case"ended":return{phase:"ended",mechanism:"auction",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(t){this.activeMonitorId=t.participation.id,this.deps.auctions.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate,t.onDegradedChange)}stopMonitoring(){this.activeMonitorId&&(this.deps.auctions.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async enter(){await this.deps.runSerializedOperation(async()=>{await this.enterWithinOperation()})}async enterWithinOperation(){const t=this.currentAuctionSequence("enterable","No open auction to enter").distribution;await this.deps.auctions.enter(t.id,void 0),await this.deps.auctions.status(t.id);const i={mechanism:"auction",status:"not_bid",id:e=t.id,distributionId:e};var e;this.deps.ensureDisplayAtom({id:t.id,type:"auction"}),this.deps.commit(r({distribution:t,consumer:i})),this.deps.emitEvent("sequence_change","success","user","Entered auction",{distributionId:t.id})}async leave(){await this.deps.runSerializedOperation(async()=>{await this.leaveWithinOperation()})}async leaveWithinOperation(){const t=this.currentAuctionSequence("participating"),i=t.consumer.distributionId??t.consumer.id??t.distribution.id;await this.deps.auctions.leave(i),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"left"}}),this.deps.emitEvent("sequence_change","info","user","Left participation",{type:"auction"})}async bid(t){await this.deps.runSerializedOperation(async()=>{const i=this.currentAuctionSequenceForBid(),s=i.consumer.distributionId??i.consumer.id??i.distribution.id,o=await this.deps.auctions.bid(s,t);if("won"===o.status||"lost"===o.status){const t=this.deps.getCurrentSequence();return void(t&&("enterable"===t.phase||"participating"===t.phase||"settling"===t.phase)&&"mechanism"in t&&"auction"===t.mechanism&&t.distribution.id===s&&("won"===o.status&&o.admissionGrant?this.deps.applyMonitorUpdate({type:"granted",token:o.admissionGrant}):this.deps.applyMonitorUpdate({type:"ended",outcome:{type:o.status}})))}const a=function(t){switch(t){case"not_bid":case"bidding":case"winning":case"outbid":return t;case"won":case"lost":return}}(o.status);if(void 0===a)return;const c="auction"===i.distribution.details?.type?i.distribution.details:void 0,u=c?.bidIncrement,d=o.highestBid&&u&&e(o.highestBid)&&e(u)?n(o.highestBid,u):void 0,p=this.deps.getDisplayState(),h="auction"===p?.type?p.settleAt:void 0,m=this.deps.ensureDisplayAtom({id:s,type:"auction"});"enterable"===i.phase&&this.deps.commit(r({distribution:i.distribution,consumer:{mechanism:"auction",status:a,id:s,distributionId:s,currentBid:t,highestBid:o.highestBid}})),m.set({type:"auction",currentBid:t,highestBid:o.highestBid,bidCount:o.bidCount,status:a,currencyCode:c?.currencyCode,bidIncrement:u,minNextBid:d,closeAt:c?.closeAt??c?.endsAt,settleAt:h})})}currentAuctionSequence(t,e="Not participating in an auction"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==t||!("mechanism"in n)||"auction"!==n.mechanism)throw i.validationError(e);return n}currentAuctionSequenceForBid(){const t=this.deps.getCurrentSequence();if(!t||"enterable"!==t.phase&&"participating"!==t.phase||!("mechanism"in t)||"auction"!==t.mechanism)throw i.validationError("No auction is available to bid on");return t}}function p(t){return{status:"not_bid",distributionId:t.id}}function h(t,i,e){return{phase:"ended",mechanism:"auction",distribution:t,consumer:i,outcome:{...e,distributionType:e.distributionType??t.type,distributionId:e.distributionId??t.id,at:e.at??/* @__PURE__ */(new Date).toISOString()}}}export{a as createAuctionDriver,o as isUpgradeableGenericLoss,r as projectAuctionSequence,c as withAuctionGrantedConsumer,u as withAuctionTerminalConsumer};
|
|
@@ -41,6 +41,7 @@ export declare class AuctionManagementModule implements AuctionModule {
|
|
|
41
41
|
private applyTransition;
|
|
42
42
|
private checkOutbidConditions;
|
|
43
43
|
private buildBidTransition;
|
|
44
|
+
private buildTerminalTransition;
|
|
44
45
|
private buildStatusTransition;
|
|
45
46
|
private buildMissingTransition;
|
|
46
47
|
get(auctionId: string): Promise<AuctionDetails>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as s}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{isMoneyString as a,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as u,isMoneyLessThanOrEqualTo as d,subtractMoney as h}from"../core/money.js";import{parseResponse as c}from"../core/parse-response.js";import{createPollScheduler as l}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as g}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as p}from"../state/events.js";import{getSDKStore as m}from"../state/store.js";class b{constructor(t){this.logger=n(),this.events=p(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollSchedulers=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.hasSuccessfulReadThisSession=/* @__PURE__ */new Map,this.lastSettleAtMs=/* @__PURE__ */new Map,this.visibilityHandlers=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new g,this.bidHistoryCache=/* @__PURE__ */new Map,this.auctionDetailsCache=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.BID_HISTORY_CACHE_TTL=5e3,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.NO_BID_END_GRACE_MS=5e3,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return m()}computeMinNextBid(t,i){if(i&&a(t)&&a(i))return o(t,i)}async getAuctionDetailsCached(t){const i=this.auctionDetailsCache.get(t);if(i&&Date.now()-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL)return i.details;const e=await this.http.get(`/auctions/${t}`);return this.auctionDetailsCache.set(t,{details:e,fetchedAt:Date.now()}),e}getTrackedParticipation(t){return this.trackedParticipations.get(t)}getAuctionCloseAt(t){return t.closeAt??t.settleAt}getAuctionSettleAt(t){return t.settleAt??t.closeAt}buildDisplayState(t,{status:i,highestBid:e,currentBid:s,bidCount:n}){return{highestBid:e,currentBid:s,bidCount:n,closeAt:this.getAuctionCloseAt(t),settleAt:t.settleAt,status:i,currencyCode:t.currencyCode,minNextBid:e?this.computeMinNextBid(e,t.minBidIncrement):void 0,bidIncrement:t.minBidIncrement??void 0}}emitTransitionEvents(t){for(const i of t)this.events.emit(i.type,i.payload)}applyTransition(t,i){i.trackedParticipation?this.trackedParticipations.set(t,i.trackedParticipation):this.trackedParticipations.delete(t);const e=this.monitorRuntime.getDisplayAtom(t);e&&i.displayState&&e.set({type:"auction",...i.displayState}),this.emitTransitionEvents(i.events),i.monitorUpdate&&this.monitorRuntime.notify(t,i.monitorUpdate),i.stopWatching&&this.stopWatching(t),i.autoRebidStatus&&this.handleAutoRebid(t,i.autoRebidStatus)}checkOutbidConditions(t,i,e){if("winning"===t.status&&"outbid"===i.status)return!0;if(e?.currentBid&&i.highestBid){const s=e.currentBid,n=i.highestBid,o=t.highestBid||"0";if(a(s)&&a(n)&&a(o))return r(n,o)&&r(n,s)&&"winning"!==i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,s,n){const a="winning"===i.status?"winning":"outbid"===i.status?"outbid":"bidding";return{trackedParticipation:{auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:a,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:n?.metadata,fingerprint:n?.fingerprint},displayState:this.buildDisplayState(s,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[{type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}},..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:e,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:e,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:s,previousStatus:n}){const a=i.currentBid??s?.currentBid,o=i.bidCount??s?.bidCount;if("won"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"won",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:a??""}}],monitorUpdate:i.admissionGrant?{type:"granted",token:i.admissionGrant}:{type:"ended",outcome:{type:"won",distributionType:"auction",distributionId:t}},stopWatching:!0,autoRebidStatus:null};if("lost"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"lost",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:i.highestBid}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null};const r=e.closeAt?new Date(e.closeAt).getTime():void 0;if("watching"===i.status&&void 0!==r&&Date.now()>=r+this.NO_BID_END_GRACE_MS)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"ended",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"ended"}},stopWatching:!0,autoRebidStatus:null};const u={auctionId:t,enteredAt:s?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:a,highestBid:i.highestBid,bidCount:o,metadata:s?.metadata,fingerprint:s?.fingerprint},d=[];let h=null;return n&&(this.checkOutbidConditions(n,i,s)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:n.currentBid||s?.currentBid||"",highestBid:i.highestBid}}),h=i),"winning"!==n.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:a||"",highestBid:i.highestBid}}),n.highestBid!==i.highestBid&&"winning"!==i.status&&d.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:u,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:a,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:h}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0,autoRebidStatus:null}}async get(t){try{return await this.getAuctionDetailsCached(t)}catch(i){throw this.logger.error("Failed to get auction",{auctionId:t,error:i}),i}}async bid(e,n,o){try{const r=this.lifecycleGeneration;if(!a(n))throw s.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let d=this.bumpOperationGeneration(e),h=this.getTrackedParticipation(e);h||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),h=this.getTrackedParticipation(e),d=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:n});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,b]=await Promise.all([this.http.post(l,{amount:n},o),this.http.get(g),this.getAuctionDetailsCached(e)]),y=c(t,p,{endpoint:l},{throwOnFailure:!0});if(!y.ok)throw y.error;const f=c(i,m,{endpoint:g},{throwOnFailure:!0});if(!f.ok)throw f.error;const w=y.value,B=f.value,S=u(B.amount),I={status:"WINNING"===w.status||"WON"===w.status?"winning":"OUTBID"===w.status||"BIDDING"===w.status||"LOST"===w.status?"outbid":"accepted",amount:n,highestBid:S,bidCount:w.bidCount??(h?.bidCount||0)+1};return this.canApplyAsyncResult(e,r,void 0,d)?(this.applyTransition(e,this.buildBidTransition(e,I,n,b,h)),I):I}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:n,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const n=this.lifecycleGeneration,a=this.store.session;if(!a)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const o=this.bumpOperationGeneration(t);if(this.logger.info("Entering auction",{auctionId:t}),await this.http.post(`/auctions/${t}/enter`,{metadata:i},e),!this.canApplyAsyncResult(t,n,void 0,o))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching",metadata:i,fingerprint:a.deviceFingerprint}),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}}])}catch(n){throw this.logger.error("Failed to enter auction",{auctionId:t,error:n}),this.events.emit("auction:error",{auctionId:t,error:n}),n}}async leave(t,i){const e=this.getTrackedParticipation(t);if(e&&(void 0!==e.currentBid||"watching"!==e.status))throw s.cannotLeaveAfterBid(t);try{const e=this.lifecycleGeneration,s=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch(n){if(404!==n?.status)throw n;this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,e,void 0,s))return;this.stopMonitoring(t),this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(n){throw this.logger.error("Failed to leave auction",{auctionId:t,error:n}),this.events.emit("auction:error",{auctionId:t,error:n}),n}}async status(t){const i=this.inFlightRequests.get(t);if(i)return this.logger.debug("Returning existing in-flight request",{auctionId:t}),i;const e=this.lifecycleGeneration,s=this.bumpOperationGeneration(t),n=this.doStatus(t,e,void 0,s).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,n),n}async doStatus(e,s,n,a){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[d,h,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=c(t,d,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=c(i,h,{endpoint:r},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=g.value,b=p.value,y=this.getAuctionSettleAt(l),f=y?new Date(y).getTime():Number.NaN,w=u(b.amount),B=this.getTrackedParticipation(e),S=m.lastBidAmount??m.winningBidAmount;let I="watching";"WINNING"===m.status||"WON"===m.status?I="winning":"OUTBID"!==m.status&&"LOST"!==m.status&&"BIDDING"!==m.status||(I="outbid");const v="WON"===m.status?"won":"LOST"===m.status?"lost":void 0,A={auctionId:e,status:I,...v?{terminal:v}:{},currentBid:B?.currentBid??(S?u(S):void 0),highestBid:w,timeRemaining:Number.isFinite(f)?Math.max(0,f-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,s,n,a))return A;const C=this.buildStatusTransition(e,A,l,{previousParticipation:B,previousStatus:this.lastStatusCache.get(e)});return this.applyTransition(e,C),this.hasSuccessfulReadThisSession.set(e,!0),Number.isFinite(f)?this.lastSettleAtMs.set(e,f):this.lastSettleAtMs.delete(e),C.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,A),this.events.emit("auction:status-updated",{auctionId:e,status:A}),A}catch(o){if(o&&"object"==typeof o&&"status"in o&&404===o.status){const t={auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(e,s,n,a))return t;const i=this.buildMissingTransition();return this.applyTransition(e,i),this.lastStatusCache.delete(e),this.hasSuccessfulReadThisSession.set(e,!0),this.lastSettleAtMs.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,s,n,a))return this.lastStatusCache.get(e)??{auctionId:e,status:"watching",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:e,error:o}),this.events.emit("auction:error",{auctionId:e,error:o}),o}}async getBidHistory(t){try{const i=this.bidHistoryCache.get(t);if(i&&Date.now()-i.fetchedAt<this.BID_HISTORY_CACHE_TTL)return this.logger.debug("Returning cached bid history",{auctionId:t}),i.bids;this.logger.info("Fetching bid history",{auctionId:t});const s=`/auctions/${t}/bids/history`,n=await this.http.get(s),a=c(e,n,{endpoint:s},{throwOnFailure:!0});if(!a.ok)throw a.error;const o=a.value.map(t=>({amount:u(t.amount),timestamp:t.timestamp,isWinning:t.isHighest,isYours:!0}));return this.bidHistoryCache.set(t,{bids:o,fetchedAt:Date.now()}),this.events.emit("auction:history-fetched",{auctionId:t,bidCount:o.length}),o}catch(i){throw this.logger.error("Failed to get bid history",{auctionId:t,error:i}),this.events.emit("auction:error",{auctionId:t,error:i}),i}}startWatching(t,i=5e3,e){this.stopWatching(t);const s=this.lifecycleGeneration,n=this.bumpMonitorGeneration(t);this.hasSuccessfulReadThisSession.set(t,!1),this.lastStatusCache.delete(t),this.lastSettleAtMs.delete(t),this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const a=l({baseMs:i,suppressBackoff:()=>this.shouldSuppressBackoff(t),onDegradedChange:e,shouldContinue:()=>this.pollSchedulers.has(t)&&this.canApplyAsyncResult(t,s,n),run:async()=>{const i=this.getOperationGeneration(t);try{await this.doStatus(t,s,n,i)}catch(e){if(!this.canApplyAsyncResult(t,s,n,i))return;throw this.logger.error("Polling failed",{auctionId:t,error:e}),e}}});this.pollSchedulers.set(t,a),this.registerVisibilityPoke(t,a),a.start(),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollSchedulers.get(t);i&&(i.stop(),this.pollSchedulers.delete(t),this.unregisterVisibilityPoke(t),this.lastStatusCache.delete(t),this.hasSuccessfulReadThisSession.delete(t),this.lastSettleAtMs.delete(t),this.logger.info("Stopped watching auction",{auctionId:t}),this.events.emit("auction:watching-stopped",{auctionId:t}))}stopAllWatching(){for(const t of Array.from(this.pollSchedulers.keys()))this.stopWatching(t)}shouldSuppressBackoff(t){return!this.hasSuccessfulReadThisSession.get(t)||this.isInSettlingWindow(t)}isInSettlingWindow(t){const i=this.lastSettleAtMs.get(t);if(void 0===i)return!1;const e=this.lastStatusCache.get(t)?.terminal;return Date.now()>=i-12e4&&"won"!==e&&"lost"!==e}registerVisibilityPoke(t,i){if("undefined"==typeof document)return;const e=()=>{"visible"===document.visibilityState&&this.isInSettlingWindow(t)&&i.poke()};document.addEventListener("visibilitychange",e),this.visibilityHandlers.set(t,e)}unregisterVisibilityPoke(t){if("undefined"==typeof document)return void this.visibilityHandlers.delete(t);const i=this.visibilityHandlers.get(t);i&&(document.removeEventListener("visibilitychange",i),this.visibilityHandlers.delete(t))}bumpMonitorGeneration(t){const i=(this.monitorGenerations.get(t)??0)+1;return this.monitorGenerations.set(t,i),i}getOperationGeneration(t){return this.operationGenerations.get(t)??0}bumpOperationGeneration(t){const i=this.getOperationGeneration(t)+1;return this.operationGenerations.set(t,i),i}canApplyAsyncResult(t,i,e,s){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===s||this.getOperationGeneration(t)===s))}enableAutoRebid(t,i,e){if(!a(i)||!a(e))throw s.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(d(i,"0")||d(e,"0"))throw s.validationError("Max bid and increment must be positive");if(r(e,i))throw s.validationError("Increment cannot be greater than max bid");const n=this.getTrackedParticipation(t);if(!n)throw s.validationError("Must enter auction before enabling auto-rebid");const o=u(n.currentBid),c=h(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:c,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:c}),this.events.emit("auction:auto-rebid-enabled",{auctionId:t,maxBid:i,increment:e})}disableAutoRebid(t){const i=this.autoRebidConfigs.get(t);i&&(i.enabled=!1,this.logger.info("Auto-rebid disabled",{auctionId:t}),this.events.emit("auction:auto-rebid-disabled",{auctionId:t,finalRebidCount:i.rebidCount||0}))}getAutoRebidConfig(t){return this.autoRebidConfigs.get(t)}async handleAutoRebid(t,i){const e=this.autoRebidConfigs.get(t);if(e&&e.enabled)try{if(!a(i.highestBid)||!a(e.maxBid)||!a(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const s=o(i.highestBid,e.increment);if(r(s,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:s,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:s});this.logger.info("Placing auto-rebid",{auctionId:t,amount:s});const n=await this.bid(t,s);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=h(e.maxBid,s),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:s,remainingBudget:e.remainingBudget,status:n.status})}catch(s){this.logger.error("Auto-rebid failed",{auctionId:t,error:s}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:s})}}startMonitoring(t,i,e,s){this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching"}),this.startWatching(t,5e3,s)}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollSchedulers.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.hasSuccessfulReadThisSession.clear(),this.lastSettleAtMs.clear(),this.visibilityHandlers.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{b as AuctionManagementModule};
|
|
1
|
+
import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as s}from"../core/errors.js";import{getLogger as n}from"../core/logger.js";import{isMoneyString as a,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as u,isMoneyLessThanOrEqualTo as d,subtractMoney as h}from"../core/money.js";import{parseResponse as c}from"../core/parse-response.js";import{createPollScheduler as l}from"../core/poll-scheduler.js";import{DistributionMonitorRuntime as g}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as p}from"../state/events.js";import{getSDKStore as m}from"../state/store.js";import{isTerminalAuctionStatus as b,toAuctionConsumerStatus as y}from"./auction-status.js";class f{constructor(t){this.logger=n(),this.events=p(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollSchedulers=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.hasSuccessfulReadThisSession=/* @__PURE__ */new Map,this.lastSettleAtMs=/* @__PURE__ */new Map,this.visibilityHandlers=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new g,this.bidHistoryCache=/* @__PURE__ */new Map,this.auctionDetailsCache=/* @__PURE__ */new Map,this.monitorGenerations=/* @__PURE__ */new Map,this.operationGenerations=/* @__PURE__ */new Map,this.BID_HISTORY_CACHE_TTL=5e3,this.AUCTION_DETAILS_CACHE_TTL=3e4,this.NO_BID_END_GRACE_MS=5e3,this.destroyed=!1,this.lifecycleGeneration=0,this.http=t}get store(){return m()}computeMinNextBid(t,i){if(i&&a(t)&&a(i))return o(t,i)}async getAuctionDetailsCached(t){const i=this.auctionDetailsCache.get(t);if(i&&Date.now()-i.fetchedAt<this.AUCTION_DETAILS_CACHE_TTL)return i.details;const e=await this.http.get(`/auctions/${t}`);return this.auctionDetailsCache.set(t,{details:e,fetchedAt:Date.now()}),e}getTrackedParticipation(t){return this.trackedParticipations.get(t)}getAuctionCloseAt(t){return t.closeAt??t.settleAt}getAuctionSettleAt(t){return t.settleAt??t.closeAt}buildDisplayState(t,{status:i,highestBid:e,currentBid:s,bidCount:n}){return{highestBid:e,currentBid:s,bidCount:n,closeAt:this.getAuctionCloseAt(t),settleAt:t.settleAt,status:i,currencyCode:t.currencyCode,minNextBid:e?this.computeMinNextBid(e,t.minBidIncrement):void 0,bidIncrement:t.minBidIncrement??void 0}}emitTransitionEvents(t){for(const i of t)this.events.emit(i.type,i.payload)}applyTransition(t,i){void 0!==i.trackedParticipation&&(i.trackedParticipation?this.trackedParticipations.set(t,i.trackedParticipation):this.trackedParticipations.delete(t));const e=this.monitorRuntime.getDisplayAtom(t);e&&i.displayState&&e.set({type:"auction",...i.displayState}),this.emitTransitionEvents(i.events),i.monitorUpdate&&this.monitorRuntime.notify(t,i.monitorUpdate),i.stopWatching&&this.stopWatching(t),i.autoRebidStatus&&this.handleAutoRebid(t,i.autoRebidStatus)}checkOutbidConditions(t,i,e){if("outbid"!==i.status)return!1;if("winning"===t.status&&"outbid"===i.status)return!0;if(e?.currentBid&&i.highestBid){const s=e.currentBid,n=i.highestBid,o=t.highestBid||"0";if(a(s)&&a(n)&&a(o))return r(n,o)&&r(n,s)&&"outbid"===i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,s,n,a){const o={type:"auction:bid-placed",payload:{auctionId:t,amount:e,status:i.status}};if("won"===i.status||"lost"===i.status){const a=this.buildTerminalTransition(t,i.status,s,{currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,admissionGrant:n});return{...a,events:[o,...a.events]}}return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(s,{status:i.status,highestBid:i.highestBid,currentBid:e,bidCount:i.bidCount}),events:[o,..."winning"===i.status?[{type:"auction:winning",payload:{auctionId:t,amount:e,highestBid:i.highestBid}}]:[],..."outbid"===i.status?[{type:"auction:outbid",payload:{auctionId:t,yourBid:e,highestBid:i.highestBid}}]:[]],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null}}buildTerminalTransition(t,i,e,s){return"won"===i?{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:i,highestBid:s.highestBid,currentBid:s.currentBid,bidCount:s.bidCount}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:s.currentBid??"",admissionGrant:s.admissionGrant}}],monitorUpdate:s.admissionGrant?{type:"granted",token:s.admissionGrant}:{type:"ended",outcome:{type:"won",distributionType:"auction",distributionId:t}},stopWatching:!0,autoRebidStatus:null}:{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:i,highestBid:s.highestBid,currentBid:s.currentBid,bidCount:s.bidCount}),events:[{type:"auction:lost",payload:{auctionId:t,highestBid:s.highestBid}}],monitorUpdate:{type:"ended",outcome:{type:"lost"}},stopWatching:!0,autoRebidStatus:null}}buildStatusTransition(t,i,e,{previousParticipation:s,previousStatus:n}){const a=i.currentBid??s?.currentBid,o=i.bidCount??s?.bidCount;if(b(n?.status))return{displayState:null,events:[],monitorUpdate:null,stopWatching:!1,autoRebidStatus:null};if("won"===i.status||"lost"===i.status)return this.buildTerminalTransition(t,i.status,e,{currentBid:a,highestBid:i.highestBid,bidCount:o,admissionGrant:i.admissionGrant});const r=e.closeAt?new Date(e.closeAt).getTime():void 0;if("not_bid"===i.status&&void 0!==r&&Date.now()>=r+this.NO_BID_END_GRACE_MS)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"ended",highestBid:i.highestBid,currentBid:a,bidCount:o}),events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"ended"}},stopWatching:!0,autoRebidStatus:null};const u={auctionId:t,enteredAt:s?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:a,highestBid:i.highestBid,bidCount:o,metadata:s?.metadata,fingerprint:s?.fingerprint},d=[];let h=null;return n&&(this.checkOutbidConditions(n,i,s)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:n.currentBid||s?.currentBid||"",highestBid:i.highestBid}}),h=i),"winning"!==n.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:a||"",highestBid:i.highestBid}}),n.highestBid!==i.highestBid&&"winning"!==i.status&&d.push({type:"auction:bid-updated",payload:{auctionId:t,highestBid:i.highestBid,bidCount:i.bidCount||0}})),{trackedParticipation:u,displayState:this.buildDisplayState(e,{status:i.status,highestBid:i.highestBid,currentBid:a,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:h}}buildMissingTransition(){return{trackedParticipation:null,displayState:{status:"lost"},events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"not_participating"}},stopWatching:!0,autoRebidStatus:null}}async get(t){try{return await this.getAuctionDetailsCached(t)}catch(i){throw this.logger.error("Failed to get auction",{auctionId:t,error:i}),i}}async bid(e,n,o){try{const r=this.lifecycleGeneration;if(!a(n))throw s.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let d=this.bumpOperationGeneration(e),h=this.getTrackedParticipation(e);h||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),h=this.getTrackedParticipation(e),d=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:n});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,f]=await Promise.all([this.http.post(l,{amount:n},o),this.http.get(g),this.getAuctionDetailsCached(e)]),B=c(t,p,{endpoint:l},{throwOnFailure:!0});if(!B.ok)throw B.error;const S=c(i,m,{endpoint:g},{throwOnFailure:!0});if(!S.ok)throw S.error;const v=B.value,C=S.value,w=u(C.amount),A=y(v.status),I={status:A,amount:n,highestBid:w,bidCount:v.bidCount??(h?.bidCount||0)+1,admissionGrant:v.admissionGrant};if(!this.canApplyAsyncResult(e,r,void 0,d))return I;this.applyTransition(e,this.buildBidTransition(e,I,n,f,v.admissionGrant,h));const R=this.lastStatusCache.get(e);return b(R?.status)||this.lastStatusCache.set(e,{auctionId:e,status:A,currentBid:n,highestBid:w,timeRemaining:0,bidCount:I.bidCount,admissionGrant:v.admissionGrant}),I}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:n,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const n=this.lifecycleGeneration,a=this.store.session;if(!a)throw s.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");const o=this.bumpOperationGeneration(t);if(this.logger.info("Entering auction",{auctionId:t}),await this.http.post(`/auctions/${t}/enter`,{metadata:i},e),!this.canApplyAsyncResult(t,n,void 0,o))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"not_bid",metadata:i,fingerprint:a.deviceFingerprint}),this.lastStatusCache.delete(t),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}},{type:"experience:distribution-entered",payload:{distributionType:"auction",distributionId:t}}])}catch(n){throw this.logger.error("Failed to enter auction",{auctionId:t,error:n}),this.events.emit("auction:error",{auctionId:t,error:n}),n}}async leave(t,i){const e=this.getTrackedParticipation(t);if(e&&(void 0!==e.currentBid||"not_bid"!==e.status))throw s.cannotLeaveAfterBid(t);try{const e=this.lifecycleGeneration,s=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch(n){if(404!==n?.status)throw n;this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,e,void 0,s))return;this.stopMonitoring(t),this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(n){throw this.logger.error("Failed to leave auction",{auctionId:t,error:n}),this.events.emit("auction:error",{auctionId:t,error:n}),n}}async status(t){const i=this.inFlightRequests.get(t);if(i)return this.logger.debug("Returning existing in-flight request",{auctionId:t}),i;const e=this.lifecycleGeneration,s=this.bumpOperationGeneration(t),n=this.doStatus(t,e,void 0,s).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,n),n}async doStatus(e,s,n,a){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[d,h,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=c(t,d,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=c(i,h,{endpoint:r},{throwOnFailure:!0});if(!p.ok)throw p.error;const m=g.value,f=p.value,B=this.getAuctionSettleAt(l),S=B?new Date(B).getTime():Number.NaN,v=u(f.amount),C=this.getTrackedParticipation(e),w=m.lastBidAmount??m.winningBidAmount,A={auctionId:e,status:y(m.status),currentBid:C?.currentBid??(w?u(w):void 0),highestBid:v,timeRemaining:Number.isFinite(S)?Math.max(0,S-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,s,n,a))return A;const I=this.lastStatusCache.get(e),R=this.buildStatusTransition(e,A,l,{previousParticipation:C,previousStatus:I});return this.applyTransition(e,R),this.hasSuccessfulReadThisSession.set(e,!0),Number.isFinite(S)?this.lastSettleAtMs.set(e,S):this.lastSettleAtMs.delete(e),b(I?.status)?this.lastStatusCache.set(e,I):b(A.status)?this.lastStatusCache.set(e,A):R.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,A),this.events.emit("auction:status-updated",{auctionId:e,status:A}),A}catch(o){if(o&&"object"==typeof o&&"status"in o&&404===o.status){const t={auctionId:e,status:"not_bid",highestBid:"0.00",timeRemaining:0};if(!this.canApplyAsyncResult(e,s,n,a))return t;const i=this.lastStatusCache.get(e);if(!b(i?.status)){const t=this.buildMissingTransition();this.applyTransition(e,t),this.lastStatusCache.delete(e)}return this.hasSuccessfulReadThisSession.set(e,!0),this.lastSettleAtMs.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,s,n,a))return this.lastStatusCache.get(e)??{auctionId:e,status:"not_bid",highestBid:"0.00",timeRemaining:0};throw this.logger.error("Failed to get auction status",{auctionId:e,error:o}),this.events.emit("auction:error",{auctionId:e,error:o}),o}}async getBidHistory(t){try{const i=this.bidHistoryCache.get(t);if(i&&Date.now()-i.fetchedAt<this.BID_HISTORY_CACHE_TTL)return this.logger.debug("Returning cached bid history",{auctionId:t}),i.bids;this.logger.info("Fetching bid history",{auctionId:t});const s=`/auctions/${t}/bids/history`,n=await this.http.get(s),a=c(e,n,{endpoint:s},{throwOnFailure:!0});if(!a.ok)throw a.error;const o=a.value.map(t=>({amount:u(t.amount),timestamp:t.timestamp,isWinning:t.isHighest,isYours:!0}));return this.bidHistoryCache.set(t,{bids:o,fetchedAt:Date.now()}),this.events.emit("auction:history-fetched",{auctionId:t,bidCount:o.length}),o}catch(i){throw this.logger.error("Failed to get bid history",{auctionId:t,error:i}),this.events.emit("auction:error",{auctionId:t,error:i}),i}}startWatching(t,i=5e3,e){this.stopWatching(t);const s=this.lifecycleGeneration,n=this.bumpMonitorGeneration(t);this.hasSuccessfulReadThisSession.set(t,!1),this.lastStatusCache.delete(t),this.lastSettleAtMs.delete(t),this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const a=l({baseMs:i,suppressBackoff:()=>this.shouldSuppressBackoff(t),onDegradedChange:e,shouldContinue:()=>this.pollSchedulers.has(t)&&this.canApplyAsyncResult(t,s,n),run:async()=>{const i=this.getOperationGeneration(t);try{await this.doStatus(t,s,n,i)}catch(e){if(!this.canApplyAsyncResult(t,s,n,i))return;throw this.logger.error("Polling failed",{auctionId:t,error:e}),e}}});this.pollSchedulers.set(t,a),this.registerVisibilityPoke(t,a),a.start(),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollSchedulers.get(t);i&&(i.stop(),this.pollSchedulers.delete(t),this.unregisterVisibilityPoke(t),this.lastStatusCache.delete(t),this.hasSuccessfulReadThisSession.delete(t),this.lastSettleAtMs.delete(t),this.logger.info("Stopped watching auction",{auctionId:t}),this.events.emit("auction:watching-stopped",{auctionId:t}))}stopAllWatching(){for(const t of Array.from(this.pollSchedulers.keys()))this.stopWatching(t)}shouldSuppressBackoff(t){return!this.hasSuccessfulReadThisSession.get(t)||this.isInSettlingWindow(t)}isInSettlingWindow(t){const i=this.lastSettleAtMs.get(t);return void 0!==i&&(Date.now()>=i-12e4&&!b(this.lastStatusCache.get(t)?.status))}registerVisibilityPoke(t,i){if("undefined"==typeof document)return;const e=()=>{"visible"===document.visibilityState&&this.isInSettlingWindow(t)&&i.poke()};document.addEventListener("visibilitychange",e),this.visibilityHandlers.set(t,e)}unregisterVisibilityPoke(t){if("undefined"==typeof document)return void this.visibilityHandlers.delete(t);const i=this.visibilityHandlers.get(t);i&&(document.removeEventListener("visibilitychange",i),this.visibilityHandlers.delete(t))}bumpMonitorGeneration(t){const i=(this.monitorGenerations.get(t)??0)+1;return this.monitorGenerations.set(t,i),i}getOperationGeneration(t){return this.operationGenerations.get(t)??0}bumpOperationGeneration(t){const i=this.getOperationGeneration(t)+1;return this.operationGenerations.set(t,i),i}canApplyAsyncResult(t,i,e,s){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===s||this.getOperationGeneration(t)===s))}enableAutoRebid(t,i,e){if(!a(i)||!a(e))throw s.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(d(i,"0")||d(e,"0"))throw s.validationError("Max bid and increment must be positive");if(r(e,i))throw s.validationError("Increment cannot be greater than max bid");const n=this.getTrackedParticipation(t);if(!n)throw s.validationError("Must enter auction before enabling auto-rebid");const o=u(n.currentBid),c=h(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:c,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:c}),this.events.emit("auction:auto-rebid-enabled",{auctionId:t,maxBid:i,increment:e})}disableAutoRebid(t){const i=this.autoRebidConfigs.get(t);i&&(i.enabled=!1,this.logger.info("Auto-rebid disabled",{auctionId:t}),this.events.emit("auction:auto-rebid-disabled",{auctionId:t,finalRebidCount:i.rebidCount||0}))}getAutoRebidConfig(t){return this.autoRebidConfigs.get(t)}async handleAutoRebid(t,i){const e=this.autoRebidConfigs.get(t);if(e&&e.enabled)try{if(!a(i.highestBid)||!a(e.maxBid)||!a(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const s=o(i.highestBid,e.increment);if(r(s,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:s,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:s});this.logger.info("Placing auto-rebid",{auctionId:t,amount:s});const n=await this.bid(t,s);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=h(e.maxBid,s),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:s,remainingBudget:e.remainingBudget,status:n.status})}catch(s){this.logger.error("Auto-rebid failed",{auctionId:t,error:s}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:s})}}startMonitoring(t,i,e,s){b(this.lastStatusCache.get(t)?.status)||(this.monitorRuntime.start(t,i,e),this.trackedParticipations.has(t)||this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"not_bid"}),this.startWatching(t,5e3,s))}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollSchedulers.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.hasSuccessfulReadThisSession.clear(),this.lastSettleAtMs.clear(),this.visibilityHandlers.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{f as AuctionManagementModule};
|
|
@@ -29,6 +29,8 @@ export interface AuctionSequenceConsumerState {
|
|
|
29
29
|
/** Consumer's current bid as a decimal money string. */
|
|
30
30
|
currentBid?: string;
|
|
31
31
|
}
|
|
32
|
+
/** True while a bidder still awaits a terminal auction outcome. */
|
|
33
|
+
export declare function isUnresolvedAuctionConsumer(consumer: AuctionSequenceConsumerState): boolean;
|
|
32
34
|
/**
|
|
33
35
|
* An auction sequence as seen by one consumer.
|
|
34
36
|
*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function t(t){return"bidding"===t.status||"winning"===t.status||"outbid"===t.status}export{t as isUnresolvedAuctionConsumer};
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Public auction surface. The runtime `AuctionManagementModule`/`AuctionModule`/`AuctionParticipation`
|
|
3
3
|
* symbols from `./types` are internal and not exported here.
|
|
4
4
|
*/
|
|
5
|
-
export type { AuctionDetails, AuctionStatus, AutoRebidConfig, Bid, BidResult } from './types';
|
|
5
|
+
export type { AuctionConsumerStatus, AuctionDetails, AuctionStatus, AutoRebidConfig, Bid, BidResult } from './types';
|
package/dist/auctions/types.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { AuctionConsumerStatus } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
|
|
1
2
|
import { RequestOptions } from '../core/http';
|
|
3
|
+
export type { AuctionConsumerStatus } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
|
|
2
4
|
/**
|
|
3
5
|
* Auction participation stored in state
|
|
4
6
|
*/
|
|
5
7
|
export interface AuctionParticipation {
|
|
6
8
|
auctionId: string;
|
|
7
9
|
enteredAt: string;
|
|
8
|
-
status:
|
|
10
|
+
status: AuctionConsumerStatus;
|
|
9
11
|
currentBid?: string;
|
|
10
12
|
highestBid?: string;
|
|
11
13
|
bidCount?: number;
|
|
@@ -28,10 +30,11 @@ export interface Bid {
|
|
|
28
30
|
* Result of placing a bid
|
|
29
31
|
*/
|
|
30
32
|
export interface BidResult {
|
|
31
|
-
status:
|
|
33
|
+
status: AuctionConsumerStatus;
|
|
32
34
|
amount: string;
|
|
33
35
|
highestBid: string;
|
|
34
36
|
bidCount: number;
|
|
37
|
+
admissionGrant?: string;
|
|
35
38
|
position?: number;
|
|
36
39
|
}
|
|
37
40
|
/**
|
|
@@ -39,13 +42,7 @@ export interface BidResult {
|
|
|
39
42
|
*/
|
|
40
43
|
export interface AuctionStatus {
|
|
41
44
|
auctionId: string;
|
|
42
|
-
status:
|
|
43
|
-
/**
|
|
44
|
-
* The server-resolved terminal outcome for this bidder, or absent while the auction is
|
|
45
|
-
* still in progress. The watch loop stops and finalizes on this alone — independent of the
|
|
46
|
-
* client clock and the admission grant.
|
|
47
|
-
*/
|
|
48
|
-
terminal?: "won" | "lost";
|
|
45
|
+
status: AuctionConsumerStatus;
|
|
49
46
|
currentBid?: string;
|
|
50
47
|
highestBid: string;
|
|
51
48
|
timeRemaining: number;
|
|
@@ -109,6 +109,14 @@ export declare class AuthenticationModule implements AuthModule {
|
|
|
109
109
|
* session (back-compat). Returns `{}` when no proof is available.
|
|
110
110
|
*/
|
|
111
111
|
private sessionMintProofHeaders;
|
|
112
|
+
/**
|
|
113
|
+
* Compute the durable identity horizon (now + the refresh token's TTL), or
|
|
114
|
+
* `undefined` when the server response omitted `refreshTokenTtlSeconds` (an
|
|
115
|
+
* older server, back-compat).
|
|
116
|
+
*/
|
|
117
|
+
private computeIdentityExpiresAt;
|
|
118
|
+
/** Set the computed identity horizon on a freshly-minted session, in place. */
|
|
119
|
+
private attachIdentityExpiresAt;
|
|
112
120
|
/**
|
|
113
121
|
* Set the refresh token in the in-memory store AND mirror it to its durable
|
|
114
122
|
* IndexedDB home (or clear that home on null), so a reload or another tab can
|
package/dist/auth/auth.module.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{FanfareError as e}from"../core/errors.js";import{getLogger as t}from"../core/logger.js";import{loadRefreshToken as s,clearRefreshToken as r,clearLegacySessionTokens as o,buildAdmissionProofHeaders as i,persistRefreshToken as n}from"../security/admission-proof.js";import{getEventBus as a}from"../state/events.js";import{getSDKStore as h}from"../state/store.js";import{BroadcastChannelTransport as c}from"../sync/broadcast-transport.js";function g(e){const t={};for(const[s,r]of Object.entries(e))"string"==typeof r&&(t[s]=r);return t}class f{constructor(e,s){this.logger=t(),this.store=h(),this.events=a(),this.lastBroadcastTokens=null,this.http=e,this.beaconHttp=s,this.setupTokenChannel(),this.http.updateConfig({...this.http.getConfig(),onUnauthorized:async()=>{await this.handleUnauthorized()}});const r=this.store.beaconToken;r&&this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":r}})}setupTokenChannel(){const e=new c("fanfare-auth-tokens");e.isAvailable()&&(this.authChannel=e,e.onMessage(e=>{const t=e.data;t&&"string"==typeof t.accessToken&&"string"==typeof t.beaconToken&&"string"==typeof t.refreshToken&&(this.lastBroadcastTokens={accessToken:t.accessToken,beaconToken:t.beaconToken,refreshToken:t.refreshToken})}))}broadcastSessionTokens(e){this.authChannel?.send(e)}check(){const e=this.store.session;return e?{isAuthenticated:!0,session:e,isGuest:"guest"===e.type}:{isAuthenticated:!1}}async guest(){try{this.logger.info("Creating guest session");const e=await this.sessionMintProofHeaders("/auth/guest"),{session:t,accessToken:s,refreshToken:r,beaconToken:o}=await this.http.post("/auth/guest",void 0,{headers:e});return this.store.setSession(t),await this.setRefreshTokenDurable(r??null),this.store.setBeaconToken(o),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${s}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":o}}),this.scheduleTokenRefresh(t.expiresAt),this.events.emit("auth:authenticated",{session:t,isNew:!0}),t}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Failed to create guest session",t),this.events.emit("auth:error",{error:t,context:"guest"}),t}}async requestOtp(e){try{this.logger.info("Requesting OTP");const t="string"==typeof e?{email:e}:e;await this.http.post("/auth/otp/request",t)}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to request OTP",e),this.events.emit("auth:error",{error:e,context:"requestOtp"}),e}}async verifyOtp(e){try{const t=e;this.logger.info("Verifying OTP",{email:t.email,phone:t.phone});const s=await this.sessionMintProofHeaders("/auth/otp/verify"),{session:r,accessToken:o,refreshToken:i,beaconToken:n}=await this.http.post("/auth/otp/verify",t,{headers:s});return this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to verify OTP",e),this.events.emit("auth:error",{error:e,context:"verifyOtp"}),e}}async exchangeExternal(e){try{const t="string"==typeof e?{exchangeCode:e}:e;this.logger.info("Exchanging external auth code");const s=await this.sessionMintProofHeaders("/auth/external/exchange"),{session:r,accessToken:o,refreshToken:i,beaconToken:n}=await this.http.post("/auth/external/exchange",t,{headers:s});return this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to exchange external auth code",e),this.events.emit("auth:error",{error:e,context:"exchangeExternal"}),e}}async login(e){await this.requestOtp({email:e.email})}async logout(){try{if(!this.store.session)return;const e=this.store.refreshToken??await s();await this.http.post("/auth/logout",e?{refreshToken:e}:{},{skipUnauthorizedHandler:!0}).catch(()=>{}),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"user"})}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Error during logout",t),this.store.clearAll(),this.lastBroadcastTokens=null,await r(),t}}async clearAuthState(){this.cancelTokenRefresh(),this.store.clearAll(),this.lastBroadcastTokens=null,await r();const e=this.http.getConfig(),{Authorization:t,...s}=e.headers||{};this.http.updateConfig({...e,headers:s});const o=this.beaconHttp.getConfig(),{"X-Beacon-Token":i,...n}=o.headers||{};this.beaconHttp.updateConfig({...o,headers:n})}getSession(){return this.store.session}async rehydrate(){const e=await s();if(o(),!e)return;if(this.store.setRefreshToken(e),!this.hasBearer()){try{await this.refresh()}catch(r){this.logger.debug("Eager rehydrate refresh failed; falling back to reactive 401",{error:r instanceof Error?r.message:String(r)})}return}const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt)}hasBearer(){return Boolean(this.http.getConfig().headers?.Authorization)}async refresh(){if("undefined"!=typeof navigator&&"locks"in navigator){const e=await s(),t=e??this.store.refreshToken;return navigator.locks.request("fanfare-refresh",()=>this._doRefresh(t,e))}return this.refreshPromise?(this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise):(this.refreshPromise=this._doRefresh().finally(()=>{this.refreshPromise=void 0}),this.refreshPromise)}absoluteAuthUrl(e){return new URL(e,this.http.getConfig().baseUrl).toString()}async sessionMintProofHeaders(e){const t=await i({url:this.absoluteAuthUrl(e),method:"POST"});return t?g(t):{}}async setRefreshTokenDurable(e){this.store.setRefreshToken(e),e?await n(e):await r()}applySessionTokens(e){this.store.setBeaconToken(e.beaconToken),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${e.accessToken}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":e.beaconToken}});const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt),this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session})}async _doRefresh(t,r){const o=await s(),n=o??this.store.refreshToken;if(null!==o&&o!==this.store.refreshToken&&this.store.setRefreshToken(o),!n)throw this.logger.info("No refresh token available; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("No refresh token available");if(r&&null===o)throw this.logger.debug("Durable refresh token cleared by a concurrent caller; skipping re-POST"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("Refresh token cleared by a concurrent caller");if(void 0!==t&&null!==o&&o!==t){this.logger.debug("Refresh already completed by a concurrent caller; adopting rotated session");const e=this.lastBroadcastTokens;return void(e&&e.refreshToken===o?this.applySessionTokens(e):this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session}))}const a=await i({url:this.absoluteAuthUrl("/auth/refresh"),method:"POST"});if(!a)throw this.logger.info("No DPoP proof available for refresh; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("DPoP proof unavailable for refresh");try{this.logger.info("Refreshing access token");const{accessToken:e,refreshToken:t,beaconToken:s,accessTokenExpiresAt:r}=await this.http.post("/auth/refresh",{refreshToken:n},{headers:g(a),skipUnauthorizedHandler:!0});await this.setRefreshTokenDurable(t??null),t&&this.broadcastSessionTokens({accessToken:e,beaconToken:s,refreshToken:t}),r&&this.store.updateSessionExpiry(r),this.applySessionTokens({accessToken:e,beaconToken:s})}catch(h){const t=h instanceof Error?h:new Error(String(h)),s=h instanceof e?h.status:void 0;throw 401===s?(this.logger.error("Refresh rejected as unauthorized; clearing auth state",t),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"})):this.logger.warn("Refresh failed transiently; preserving durable refresh token",{status:s,error:t.message}),t}}async handleUnauthorized(){this.logger.warn("Received 401, attempting to refresh token");try{await this.refresh()}catch(e){const t=e instanceof Error?e:new Error(String(e));this.logger.error("Failed to handle 401",t),this.events.emit("auth:error",{error:new Error("Session expired"),context:"unauthorized"})}}scheduleTokenRefresh(e){if(this.cancelTokenRefresh(),!this.store.refreshToken)return;const t=Date.now(),s=new Date(e).getTime(),r=Number.isFinite(s),o=r?Math.max(s-3e5-t,3e4):3e4;this.logger.debug("Scheduling token refresh",{expiresAt:r?new Date(s).toISOString():"invalid",delayMs:o}),this.refreshTimer=setTimeout(()=>{this.refresh().catch(e=>{this.logger.error("Scheduled token refresh failed",e)})},o)}cancelTokenRefresh(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0,this.logger.debug("Cancelled token refresh timer"))}destroy(){this.cancelTokenRefresh(),this.refreshPromise=void 0,this.authChannel?.close(),this.authChannel=void 0}}export{f as AuthenticationModule};
|
|
1
|
+
import{FanfareError as e}from"../core/errors.js";import{getLogger as t}from"../core/logger.js";import{loadRefreshToken as s,clearRefreshToken as r,clearLegacySessionTokens as o,buildAdmissionProofHeaders as i,persistRefreshToken as n}from"../security/admission-proof.js";import{getEventBus as a}from"../state/events.js";import{getSDKStore as h}from"../state/store.js";import{BroadcastChannelTransport as c}from"../sync/broadcast-transport.js";function f(e){const t={};for(const[s,r]of Object.entries(e))"string"==typeof r&&(t[s]=r);return t}class g{constructor(e,s){this.logger=t(),this.store=h(),this.events=a(),this.lastBroadcastTokens=null,this.http=e,this.beaconHttp=s,this.setupTokenChannel(),this.http.updateConfig({...this.http.getConfig(),onUnauthorized:async()=>{await this.handleUnauthorized()}});const r=this.store.beaconToken;r&&this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":r}})}setupTokenChannel(){const e=new c("fanfare-auth-tokens");e.isAvailable()&&(this.authChannel=e,e.onMessage(e=>{const t=e.data;t&&"string"==typeof t.accessToken&&"string"==typeof t.beaconToken&&"string"==typeof t.refreshToken&&(this.lastBroadcastTokens={accessToken:t.accessToken,beaconToken:t.beaconToken,refreshToken:t.refreshToken})}))}broadcastSessionTokens(e){this.authChannel?.send(e)}check(){const e=this.store.session;return e?{isAuthenticated:!0,session:e,isGuest:"guest"===e.type,identityExpiresAt:e.identityExpiresAt}:{isAuthenticated:!1}}async guest(){try{this.logger.info("Creating guest session");const e=await this.sessionMintProofHeaders("/auth/guest"),{session:t,accessToken:s,refreshToken:r,beaconToken:o,refreshTokenTtlSeconds:i}=await this.http.post("/auth/guest",void 0,{headers:e});return this.attachIdentityExpiresAt(t,i),this.store.setSession(t),await this.setRefreshTokenDurable(r??null),this.store.setBeaconToken(o),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${s}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":o}}),this.scheduleTokenRefresh(t.expiresAt),this.events.emit("auth:authenticated",{session:t,isNew:!0}),t}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Failed to create guest session",t),this.events.emit("auth:error",{error:t,context:"guest"}),t}}async requestOtp(e){try{this.logger.info("Requesting OTP");const t="string"==typeof e?{email:e}:e;await this.http.post("/auth/otp/request",t)}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to request OTP",e),this.events.emit("auth:error",{error:e,context:"requestOtp"}),e}}async verifyOtp(e){try{const t=e;this.logger.info("Verifying OTP",{email:t.email,phone:t.phone});const s=await this.sessionMintProofHeaders("/auth/otp/verify"),{session:r,accessToken:o,refreshToken:i,beaconToken:n,refreshTokenTtlSeconds:a}=await this.http.post("/auth/otp/verify",t,{headers:s});return this.attachIdentityExpiresAt(r,a),this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to verify OTP",e),this.events.emit("auth:error",{error:e,context:"verifyOtp"}),e}}async exchangeExternal(e){try{const t="string"==typeof e?{exchangeCode:e}:e;this.logger.info("Exchanging external auth code");const s=await this.sessionMintProofHeaders("/auth/external/exchange"),{session:r,accessToken:o,refreshToken:i,beaconToken:n,refreshTokenTtlSeconds:a}=await this.http.post("/auth/external/exchange",t,{headers:s});return this.attachIdentityExpiresAt(r,a),this.store.setSession(r),await this.setRefreshTokenDurable(i??null),this.store.setBeaconToken(n),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${o}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":n}}),this.scheduleTokenRefresh(r.expiresAt),this.events.emit("auth:authenticated",{session:r,isNew:!0}),r}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to exchange external auth code",e),this.events.emit("auth:error",{error:e,context:"exchangeExternal"}),e}}async login(e){await this.requestOtp({email:e.email})}async logout(){try{if(!this.store.session)return;const e=this.store.refreshToken??await s();await this.http.post("/auth/logout",e?{refreshToken:e}:{},{skipUnauthorizedHandler:!0}).catch(()=>{}),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"user"})}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this.logger.error("Error during logout",t),this.store.clearAll(),this.lastBroadcastTokens=null,await r(),t}}async clearAuthState(){this.cancelTokenRefresh(),this.store.clearAll(),this.lastBroadcastTokens=null,await r();const e=this.http.getConfig(),{Authorization:t,...s}=e.headers||{};this.http.updateConfig({...e,headers:s});const o=this.beaconHttp.getConfig(),{"X-Beacon-Token":i,...n}=o.headers||{};this.beaconHttp.updateConfig({...o,headers:n})}getSession(){return this.store.session}async rehydrate(){const e=await s();if(o(),!e)return;if(this.store.setRefreshToken(e),!this.hasBearer()){try{await this.refresh()}catch(r){this.logger.debug("Eager rehydrate refresh failed; falling back to reactive 401",{error:r instanceof Error?r.message:String(r)})}return}const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt)}hasBearer(){return Boolean(this.http.getConfig().headers?.Authorization)}async refresh(){if("undefined"!=typeof navigator&&"locks"in navigator){const e=await s(),t=e??this.store.refreshToken;return navigator.locks.request("fanfare-refresh",()=>this._doRefresh(t,e))}return this.refreshPromise?(this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise):(this.refreshPromise=this._doRefresh().finally(()=>{this.refreshPromise=void 0}),this.refreshPromise)}absoluteAuthUrl(e){return new URL(e,this.http.getConfig().baseUrl).toString()}async sessionMintProofHeaders(e){const t=await i({url:this.absoluteAuthUrl(e),method:"POST"});return t?f(t):{}}computeIdentityExpiresAt(e){if("number"==typeof e&&Number.isFinite(e))return new Date(Date.now()+1e3*e).toISOString()}attachIdentityExpiresAt(e,t){const s=this.computeIdentityExpiresAt(t);s&&(e.identityExpiresAt=s)}async setRefreshTokenDurable(e){this.store.setRefreshToken(e),e?await n(e):await r()}applySessionTokens(e){this.store.setBeaconToken(e.beaconToken),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${e.accessToken}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":e.beaconToken}});const t=this.store.session;t?.expiresAt&&this.scheduleTokenRefresh(t.expiresAt),this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session})}async _doRefresh(t,r){const o=await s(),n=o??this.store.refreshToken;if(null!==o&&o!==this.store.refreshToken&&this.store.setRefreshToken(o),!n)throw this.logger.info("No refresh token available; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("No refresh token available");if(r&&null===o)throw this.logger.debug("Durable refresh token cleared by a concurrent caller; skipping re-POST"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("Refresh token cleared by a concurrent caller");if(void 0!==t&&null!==o&&o!==t){this.logger.debug("Refresh already completed by a concurrent caller; adopting rotated session");const e=this.lastBroadcastTokens;return void(e&&e.refreshToken===o?this.applySessionTokens(e):this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session}))}const a=await i({url:this.absoluteAuthUrl("/auth/refresh"),method:"POST"});if(!a)throw this.logger.info("No DPoP proof available for refresh; clearing auth state"),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("DPoP proof unavailable for refresh");try{this.logger.info("Refreshing access token");const{accessToken:e,refreshToken:t,beaconToken:s,accessTokenExpiresAt:r,refreshTokenTtlSeconds:o}=await this.http.post("/auth/refresh",{refreshToken:n},{headers:f(a),skipUnauthorizedHandler:!0});if(await this.setRefreshTokenDurable(t??null),t&&this.broadcastSessionTokens({accessToken:e,beaconToken:s,refreshToken:t}),r){const e=this.computeIdentityExpiresAt(o);e?this.store.updateSessionExpiry(r,e):this.store.updateSessionExpiry(r)}this.applySessionTokens({accessToken:e,beaconToken:s})}catch(h){const t=h instanceof Error?h:new Error(String(h)),s=h instanceof e?h.status:void 0;throw 401===s?(this.logger.error("Refresh rejected as unauthorized; clearing auth state",t),await this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"})):this.logger.warn("Refresh failed transiently; preserving durable refresh token",{status:s,error:t.message}),t}}async handleUnauthorized(){this.logger.warn("Received 401, attempting to refresh token");try{await this.refresh()}catch(e){const t=e instanceof Error?e:new Error(String(e));this.logger.error("Failed to handle 401",t),this.events.emit("auth:error",{error:new Error("Session expired"),context:"unauthorized"})}}scheduleTokenRefresh(e){if(this.cancelTokenRefresh(),!this.store.refreshToken)return;const t=Date.now(),s=new Date(e).getTime(),r=Number.isFinite(s),o=r?Math.max(s-3e5-t,3e4):3e4;this.logger.debug("Scheduling token refresh",{expiresAt:r?new Date(s).toISOString():"invalid",delayMs:o}),this.refreshTimer=setTimeout(()=>{this.refresh().catch(e=>{this.logger.error("Scheduled token refresh failed",e)})},o)}cancelTokenRefresh(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0,this.logger.debug("Cancelled token refresh timer"))}destroy(){this.cancelTokenRefresh(),this.refreshPromise=void 0,this.authChannel?.close(),this.authChannel=void 0}}export{g as AuthenticationModule};
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -73,6 +73,11 @@ export interface AuthStatus {
|
|
|
73
73
|
isAuthenticated: boolean;
|
|
74
74
|
isGuest?: boolean;
|
|
75
75
|
session?: Session;
|
|
76
|
+
/**
|
|
77
|
+
* Device-scoped horizon until which this device stays recognized, mirrored
|
|
78
|
+
* from `Session.identityExpiresAt`. See that field for details.
|
|
79
|
+
*/
|
|
80
|
+
identityExpiresAt?: string;
|
|
76
81
|
}
|
|
77
82
|
/**
|
|
78
83
|
* Base session interface
|
|
@@ -84,6 +89,16 @@ export interface Session {
|
|
|
84
89
|
phone?: string;
|
|
85
90
|
expiresAt: string;
|
|
86
91
|
deviceFingerprint?: string;
|
|
92
|
+
/**
|
|
93
|
+
* ISO timestamp of the durable identity horizon: how long this device stays
|
|
94
|
+
* recognized via its refresh token, distinct from `expiresAt` (the short-lived
|
|
95
|
+
* access token). Computed client-side as now + the server's
|
|
96
|
+
* `refreshTokenTtlSeconds` at the moment the session is minted or refreshed —
|
|
97
|
+
* a refresh rotates the refresh token and re-arms this horizon. Suitable for
|
|
98
|
+
* copy like "you'll be remembered on this device until X". Absent when the
|
|
99
|
+
* server response omitted `refreshTokenTtlSeconds`.
|
|
100
|
+
*/
|
|
101
|
+
identityExpiresAt?: string;
|
|
87
102
|
}
|
|
88
103
|
/**
|
|
89
104
|
* Guest session
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpClient } from '../core/http';
|
|
2
|
-
import { BeaconConfig, BeaconEventInput, BeaconModule } from './types';
|
|
2
|
+
import { BeaconConfig, BeaconEventInput, BeaconModule, BeaconSequenceResolver, BoundBeaconTracker } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Beacon tracking module for client-side event tracking
|
|
5
5
|
*
|
|
@@ -12,7 +12,19 @@ export declare class BeaconTrackingModule implements BeaconModule {
|
|
|
12
12
|
private store;
|
|
13
13
|
private batcher;
|
|
14
14
|
private config;
|
|
15
|
-
|
|
15
|
+
private resolveSequenceId?;
|
|
16
|
+
constructor(http: HttpClient, config?: BeaconConfig, resolveSequenceId?: BeaconSequenceResolver);
|
|
17
|
+
/**
|
|
18
|
+
* Bind a tracker to an experience.
|
|
19
|
+
*
|
|
20
|
+
* The bound tracker stamps every event with the experience — and, once the
|
|
21
|
+
* journey has routed, the sequence — resolving and merging attribution as each
|
|
22
|
+
* event is captured (not at flush), so a reroute during the batching window can
|
|
23
|
+
* never rewrite already-recorded attribution. The tracker's event type omits
|
|
24
|
+
* `experienceId`/`sequenceId`, so a caller cannot supply attribution that would
|
|
25
|
+
* disagree with the binding.
|
|
26
|
+
*/
|
|
27
|
+
bind(experienceId: string): BoundBeaconTracker;
|
|
16
28
|
/**
|
|
17
29
|
* Track a single event
|
|
18
30
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{FanfareError as e,ErrorCodes as t}from"../core/errors.js";import{getLogger as r}from"../core/logger.js";import{getEventBus as n}from"../state/events.js";import{getSDKStore as s}from"../state/store.js";import{EventBatcher as o}from"./batching.js";import{enrichEvent as i}from"./enrichment.js";import{validateEvent as c}from"./validation.js";class a{constructor(e,t={},i){this.logger=r(),this.events=n(),this.store=s(),this.http=e,this.config=t,this.resolveSequenceId=i,this.batcher=new o(t,async e=>{await this.sendBatch(e)}),this.logger.debug("Beacon tracking module initialized",{config:t})}bind(r){const n="string"==typeof r?r.trim():"";if(""===n)throw new e("beacon.bind requires a non-empty experienceId.",t.VALIDATION_ERROR);const s=e=>{const{experienceId:t,sequenceId:r,...s}=e,o=this.resolveSequenceId?.(n);return{...s,experienceId:n,...void 0!==o?{sequenceId:o}:{}}};return{track:e=>this.track(s(e)),trackBatch:e=>this.trackBatch(e.map(s))}}async track(e){try{const t=i(e,this.config);c(t),this.batcher.add(t),this.events.emit("beacon:queued",{event:t}),this.logger.debug("Event queued for tracking",{event:t})}catch(t){throw this.logger.error("Failed to queue event",{error:t,event:e}),this.events.emit("beacon:error",{error:t,event:e}),t}}async trackBatch(e){try{const t=e.map(e=>{const t=i(e,this.config);return c(t),t}),r=await this.sendBatch(t);this.logger[r?"info":"warn"](r?"Batch tracked successfully":"Batch queued but send failed",{count:t.length})}catch(t){throw this.logger.error("Failed to track batch",{error:t,count:e.length}),this.events.emit("beacon:error",{error:t,events:e}),t}}async flush(){await this.batcher.flush()}async sendBatch(e){if(0===e.length)return!0;try{const t=1===e.length?"/events":"/events/batch",r=1===e.length?e[0]:e,n=this.http.getConfig().headers?.["X-Organization-Id"],s="string"==typeof n?n:void 0,o=this.store.session?.consumerId,i=e=>({...e,...s?{organizationId:s}:{},...o?{consumerId:o}:{}}),c=Array.isArray(r)?r.map(i):i(r);return await this.http.post(t,c),this.events.emit("beacon:sent",{count:e.length}),this.logger.info("Events sent to beacon",{count:e.length}),!0}catch(t){return this.logger.error("Failed to send events",{error:t,count:e.length}),this.events.emit("beacon:error",{error:t,events:e}),!1}}destroy(){this.batcher.destroy()}}export{a as BeaconTrackingModule};
|
package/dist/beacon/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { BeaconTrackingModule } from './beacon.module';
|
|
10
10
|
export { enrichEvent } from './enrichment';
|
|
11
|
-
export type { BeaconConfig, BeaconEvent, BeaconEventInput, BeaconModule } from './types';
|
|
11
|
+
export type { BeaconConfig, BeaconEvent, BeaconEventInput, BeaconModule, BeaconSequenceResolver, BoundBeaconEventInput, BoundBeaconTracker, } from './types';
|
|
12
12
|
export { ValidationError, validateEvent } from './validation';
|
package/dist/beacon/public.d.ts
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
* `BeaconTrackingModule`/`enrichEvent`/`validateEvent` are deliberately not
|
|
6
6
|
* re-exported here. `ValidationError` is kept because it is on the root barrel.
|
|
7
7
|
*/
|
|
8
|
-
export type { BeaconConfig, BeaconEvent, BeaconEventInput, BeaconModule } from './types';
|
|
8
|
+
export type { BeaconConfig, BeaconEvent, BeaconEventInput, BeaconModule, BoundBeaconEventInput, BoundBeaconTracker, } from './types';
|
|
9
9
|
export { ValidationError } from './validation';
|
package/dist/beacon/types.d.ts
CHANGED
|
@@ -1,9 +1,75 @@
|
|
|
1
1
|
import { BeaconEvent, BeaconEventInput } from '@fanfare-io/fanfare-sdk-contracts/beacon';
|
|
2
2
|
export type { BeaconEvent, BeaconEventInput };
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the sequence a bound tracker should attribute events to for a given
|
|
5
|
+
* experience, returning `undefined` when that experience has not yet routed to a
|
|
6
|
+
* sequence. A bound tracker calls this at `track`-time so recorded attribution
|
|
7
|
+
* reflects the sequence in effect the moment the event was captured.
|
|
8
|
+
*/
|
|
9
|
+
export type BeaconSequenceResolver = (experienceId: string) => string | undefined;
|
|
10
|
+
/** Distributes `Omit` across a union so each member keeps its own shape. */
|
|
11
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
12
|
+
/**
|
|
13
|
+
* Event shape accepted by a bound tracker: the pre-enrichment input minus the
|
|
14
|
+
* attribution identifiers the binding owns. Omitting `experienceId`/`sequenceId`
|
|
15
|
+
* from the type means a caller cannot supply attribution that would disagree with
|
|
16
|
+
* the binding — the tracker sets both itself. The omit is distributive so the
|
|
17
|
+
* per-event discriminated union (each `eventName` paired with its own
|
|
18
|
+
* `eventProperties`) is preserved rather than collapsed into one merged shape.
|
|
19
|
+
*/
|
|
20
|
+
export type BoundBeaconEventInput = DistributiveOmit<BeaconEventInput, "experienceId" | "sequenceId">;
|
|
21
|
+
/**
|
|
22
|
+
* Tracker scoped to a single experience.
|
|
23
|
+
*
|
|
24
|
+
* Every event it records is stamped with the bound `experienceId` and, once the
|
|
25
|
+
* experience has routed, the current `sequenceId`. The identifiers are resolved
|
|
26
|
+
* and merged synchronously as each event is captured, so a later routing change
|
|
27
|
+
* never rewrites attribution already recorded. The event type omits those
|
|
28
|
+
* identifiers, so a caller cannot set them.
|
|
29
|
+
*/
|
|
30
|
+
export interface BoundBeaconTracker {
|
|
31
|
+
/**
|
|
32
|
+
* Track a single event, stamped with the bound experience/sequence attribution.
|
|
33
|
+
*
|
|
34
|
+
* @param event - Event to track; `experienceId`/`sequenceId` are set by the
|
|
35
|
+
* binding and cannot be supplied.
|
|
36
|
+
* @returns Promise that resolves when the event is queued (not sent)
|
|
37
|
+
*/
|
|
38
|
+
track(event: BoundBeaconEventInput): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Track multiple events, each stamped with the bound attribution.
|
|
41
|
+
*
|
|
42
|
+
* @param events - Events to track; `experienceId`/`sequenceId` are set by the
|
|
43
|
+
* binding and cannot be supplied.
|
|
44
|
+
* @returns Promise that resolves when the batch is sent
|
|
45
|
+
*/
|
|
46
|
+
trackBatch(events: BoundBeaconEventInput[]): Promise<void>;
|
|
47
|
+
}
|
|
3
48
|
/**
|
|
4
49
|
* Beacon tracking module interface
|
|
5
50
|
*/
|
|
6
51
|
export interface BeaconModule {
|
|
52
|
+
/**
|
|
53
|
+
* Bind a tracker to an experience so its events carry experience — and, once
|
|
54
|
+
* routed, sequence — attribution without threading the identifiers through
|
|
55
|
+
* every call.
|
|
56
|
+
*
|
|
57
|
+
* The `experienceId` is validated and fixed at bind time. The sequence is
|
|
58
|
+
* resolved from that experience's journey at `track`-time, so a tracker bound
|
|
59
|
+
* before routing records experience-only attribution and begins stamping the
|
|
60
|
+
* sequence automatically once routing completes. The unscoped `track` /
|
|
61
|
+
* `trackBatch` below stay available for events emitted outside an experience.
|
|
62
|
+
*
|
|
63
|
+
* @param experienceId - Experience to attribute events to
|
|
64
|
+
* @returns A tracker scoped to `experienceId`
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const tracker = fanfare.beacon.bind(experienceId);
|
|
69
|
+
* await tracker.track({ eventName: "productImpression" });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
bind(experienceId: string): BoundBeaconTracker;
|
|
7
73
|
/**
|
|
8
74
|
* Track a single event
|
|
9
75
|
*
|