@fanfare-io/fanfare-sdk-core 0.9.0 → 0.11.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/admission-proof.d.ts +2 -0
- package/dist/admission-proof.js +1 -0
- package/dist/appointments/appointment.driver.js +1 -1
- package/dist/appointments/appointment.module.d.ts +2 -2
- package/dist/appointments/appointment.module.js +1 -1
- package/dist/appointments/appointment.sequence.d.ts +1 -5
- package/dist/appointments/types.d.ts +2 -2
- 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 +10 -2
- package/dist/auctions/auction.module.js +1 -1
- package/dist/auctions/auction.sequence.d.ts +4 -14
- 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 +12 -0
- package/dist/core/errors.js +1 -1
- package/dist/core/http.d.ts +1 -0
- package/dist/core/http.js +1 -1
- package/dist/core/poll-scheduler.d.ts +21 -0
- package/dist/core/poll-scheduler.js +1 -0
- package/dist/core/utils.d.ts +2 -0
- package/dist/core/utils.js +1 -1
- package/dist/draws/draw.driver.js +1 -1
- package/dist/draws/draw.module.d.ts +3 -0
- package/dist/draws/draw.module.js +1 -1
- package/dist/draws/draw.sequence.d.ts +2 -6
- package/dist/draws/types.d.ts +1 -0
- package/dist/experiences/distribution-monitor.types.d.ts +2 -2
- package/dist/experiences/experience.module.d.ts +2 -1
- package/dist/experiences/experience.module.js +1 -1
- package/dist/experiences/journey-contract.fixtures.d.ts +3 -0
- package/dist/experiences/journey-view.js +1 -1
- package/dist/experiences/journey.d.ts +12 -0
- package/dist/experiences/journey.js +1 -1
- package/dist/experiences/journey.machine.d.ts +3 -0
- package/dist/experiences/journey.machine.js +1 -1
- package/dist/experiences/journey.types.d.ts +28 -16
- package/dist/experiences/sequence-driver.d.ts +7 -11
- package/dist/experiences/types.d.ts +1 -1
- package/dist/queues/queue.driver.js +1 -1
- package/dist/queues/queue.module.d.ts +5 -2
- package/dist/queues/queue.module.js +1 -1
- package/dist/queues/queue.sequence.d.ts +2 -6
- package/dist/queues/types.d.ts +1 -0
- 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 +16 -3
- package/dist/state/capability-token-registry.js +1 -1
- package/dist/state/events.d.ts +6 -5
- package/dist/state/store.d.ts +10 -2
- package/dist/state/store.js +1 -1
- package/dist/sync/cross-tab-coordinator.js +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/protocol-transport.d.ts +3 -2
- package/dist/sync/protocol-transport.js +1 -1
- package/dist/sync/protocol.d.ts +30 -1
- package/dist/sync/protocol.js +1 -1
- package/dist/test-utils/harness-scenarios.js +1 -1
- package/dist/test-utils/mock-journey.d.ts +2 -2
- package/dist/test-utils/mock-journey.js +1 -1
- package/dist/test-utils/mock-sdk.js +1 -1
- package/dist/test-utils/verification-scenarios.d.ts +1 -1
- package/dist/test-utils/verification-scenarios.js +1 -1
- package/dist/timed-releases/timed-release.driver.js +1 -1
- package/dist/timed-releases/timed-release.sequence.d.ts +2 -6
- package/dist/types/distribution-type.d.ts +10 -22
- package/dist/utils/fingerprint.module.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -10
- package/dist/handoff/handoff.module.d.ts +0 -184
- package/dist/handoff/handoff.module.js +0 -1
- package/dist/handoff/index.d.ts +0 -6
- package/dist/handoff/index.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{buildAdmissionProofHeaders as o,canonicalizeAdmissionProofUrl as r}from"./security/admission-proof.js";export{o as buildAdmissionProofHeaders,r as canonicalizeAdmissionProofUrl};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createError as t}from"../core/errors.js";function
|
|
1
|
+
import{createError as t}from"../core/errors.js";function e(t){return new o(t)}function i(e){const{distribution:i,consumer:n,outcome:o}=e;if(!i)return{phase:"unavailable"};const a=function(e){if("appointment"!==e.type)throw t.validationError("Appointment driver received a non-appointment distribution");return e}(i),r=n&&"appointment"===n.mechanism?function(t){const{mechanism:e,...i}=t;return i}(n):function(t){return{status:"not_booked",distributionId:t.id}}(i),p=o??function(t){return"completed"===t.status?{type:"completed"}:"cancelled"===t.status?{type:"cancelled"}:"no_show"===t.status?{type:"no_show"}:void 0}(r);return p?s(a,r,p):function(t){return"booked"===t.status||"checked_in"===t.status}(r)?{phase:"participating",mechanism:"appointment",distribution:a,consumer:r}:"scheduled"===a.lifecycle?{phase:"scheduled",mechanism:"appointment",distribution:a}:"open"===a.lifecycle?{phase:"enterable",mechanism:"appointment",distribution:a,consumer:r}:s(a,r,{type:"closed"})}function n(t,e){return t&&"appointment"===t.mechanism&&e?"cancelled"===e.type?{...t,status:"cancelled"}:"no_show"===e.type?{...t,status:"no_show"}:"completed"===e.type?{...t,status:"completed"}:{...t,status:"not_booked"}:t}class o{constructor(t){this.deps=t,this.mechanism="appointment"}project(t){return i(t)}buildView(e,i){if("unavailable"===e.phase)return{phase:"unavailable",reason:e.reason};const n=function(e){if(!("mechanism"in e)||"appointment"!==e.mechanism)throw t.validationError("Appointment driver received a non-appointment sequence");return e}(e);switch(n.phase){case"scheduled":return{phase:"scheduled",mechanism:"appointment",distribution:n.distribution,startsAt:n.distribution.startsAt??n.distribution.opensAt};case"enterable":return{phase:"enterable",mechanism:"appointment",distribution:n.distribution,consumer:n.consumer,book:(t,e)=>this.book(t,e)};case"participating":return{phase:"participating",mechanism:"appointment",consumer:n.consumer,display$:i.displayAtom,cancel:t=>this.cancel(t),reschedule:(t,e)=>this.reschedule(t,e)};case"ended":return{phase:"ended",mechanism:"appointment",consumer:n.consumer,outcome:n.outcome}}}startMonitoring(t){this.activeMonitorId=t.participation.id,this.deps.appointments.startMonitoring(t.participation.id,{displayAtom:t.displayAtom},t.onUpdate,t.onDegradedChange)}stopMonitoring(){this.activeMonitorId&&(this.deps.appointments.stopMonitoring(this.activeMonitorId),this.activeMonitorId=void 0)}async book(t,e){await this.deps.runSerializedOperation(async()=>{const n=this.currentAppointmentSequence("enterable","No open appointment to book").distribution,o=await this.deps.appointments.book(n.id,t,e);this.deps.appointments.bumpMonitorGeneration(n.id),this.writeBookingDisplay(n.id,o),this.deps.commit(i({distribution:n,consumer:{mechanism:"appointment",status:"booked",id:n.id,distributionId:n.id,slotId:o.slotId,locationId:o.locationId}})),this.deps.emitEvent("sequence_change","success","user","Booked appointment slot",{appointmentId:n.id,slotId:t,locationId:e})})}async cancel(e){await this.deps.runSerializedOperation(async()=>{const i=this.currentAppointmentSequence("participating"),n=i.consumer,o=this.deps.getDisplayState(),s=o&&"appointment"===o.type?o.booking:void 0;if(!s)throw t.validationError("No booking details available to cancel");const a=n.distributionId??n.id??i.distribution.id;await this.deps.appointments.cancel(a,s.slotId,s.locationId??"",e),this.deps.stopRuntimeMonitoring(!0),this.deps.applyMonitorUpdate({type:"ended",outcome:{type:"cancelled",reason:e}})})}async reschedule(t,e){await this.deps.runSerializedOperation(async()=>{const i=this.currentAppointmentSequence("participating"),n=i.consumer,o=n.distributionId??n.id??i.distribution.id,s=await this.deps.appointments.reschedule(o,t,e);this.deps.appointments.bumpMonitorGeneration(o),this.writeBookingDisplay(o,s),this.deps.emitEvent("sequence_change","info","user","Rescheduled appointment",{appointmentId:o,newSlotId:t,newLocationId:e})})}writeBookingDisplay(t,e){const i=this.deps.ensureDisplayAtom({id:t,type:"appointment"}),n=Math.max(0,Date.parse(e.startTime)-Date.now());i.set({type:"appointment",booking:{slotId:e.slotId,locationId:e.locationId,locationName:e.locationName,joinUrl:e.joinUrl,startTime:e.startTime,endTime:e.endTime,confirmationCode:e.confirmationCode,consumerStatus:e.consumerStatus},msUntilStart:n})}currentAppointmentSequence(e,i="Not participating in an appointment"){const n=this.deps.getCurrentSequence();if(!n||n.phase!==e||!("mechanism"in n)||"appointment"!==n.mechanism)throw t.validationError(i);return n}}function s(t,e,i){return{phase:"ended",mechanism:"appointment",distribution:t,consumer:e,outcome:{...i,distributionType:i.distributionType??t.type,distributionId:i.distributionId??t.id,at:i.at??/* @__PURE__ */(new Date).toISOString()}}}export{e as createAppointmentDriver,i as projectAppointmentSequence,n as withAppointmentTerminalConsumer};
|
|
@@ -6,7 +6,7 @@ export declare class AppointmentManagementModule implements AppointmentModule {
|
|
|
6
6
|
private readonly http;
|
|
7
7
|
private readonly logger;
|
|
8
8
|
private readonly monitorRuntime;
|
|
9
|
-
private readonly
|
|
9
|
+
private readonly pollSchedulers;
|
|
10
10
|
private readonly monitorGenerations;
|
|
11
11
|
private readonly pollingIntervalMs;
|
|
12
12
|
private destroyed;
|
|
@@ -23,7 +23,7 @@ export declare class AppointmentManagementModule implements AppointmentModule {
|
|
|
23
23
|
book(appointmentId: string, slotId: string, locationId?: string): Promise<AppointmentBooking>;
|
|
24
24
|
cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string): Promise<void>;
|
|
25
25
|
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string): Promise<AppointmentBooking>;
|
|
26
|
-
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
|
|
26
|
+
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
|
|
27
27
|
stopMonitoring(id: string): void;
|
|
28
28
|
isMonitoring(id: string): boolean;
|
|
29
29
|
bumpMonitorGeneration(id: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as t from"valibot";import{isFanfareError as o,createError as n}from"../core/errors.js";import{getLogger as i}from"../core/logger.js";import{parseResponse as e}from"../core/parse-response.js";import{
|
|
1
|
+
import*as t from"valibot";import{isFanfareError as o,createError as n}from"../core/errors.js";import{getLogger as i}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,n){const i=e(t,o,{endpoint:n},{throwOnFailure:!0});if(!i.ok)throw i.error;return i.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}))}),u=t.object({bookingId:t.string(),previousSlotId:t.string(),newSlotId:t.string(),confirmationCode:t.string(),status:c});function g(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=i(),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}`),n=a(m,o,`/appointments/${t}`);return{id:n.id,type:"appointment",openAt:n.openAt??void 0,closeAt:n.closeAt??void 0,slotDurationMinutes:n.slotDurationMinutes??void 0,bookingWindowOpenAt:n.openAt??void 0,bookingWindowCloseAt:n.closeAt??void 0,timezone:n.timeZone??void 0,locationId:n.locationId??void 0,totalSlots:n.totalSlots??void 0,availableSlots:n.availableSlots??void 0}}catch(i){if(this.logger.error("Failed to get appointment",{appointmentId:t,error:i}),o(i))throw i;throw n.networkError("Failed to get appointment",{error:i})}}async getSlots(t,i){try{const o=new URLSearchParams({startDate:i.startDate,endDate:i.endDate});i.timezone&&o.set("timezone",i.timezone);const n=await this.http.get(`/appointments/${t}/slots?${o.toString()}`);return a(d,n,`/appointments/${t}/slots`).slots.map(g)}catch(e){if(this.logger.error("Failed to get appointment slots",{appointmentId:t,error:e}),o(e))throw e;throw n.networkError("Failed to get appointment slots",{error:e})}}async getMe(t){try{const o=await this.http.get(`/appointments/${t}/me`),n=a(h,o,`/appointments/${t}/me`);return n.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}}(n.booking):null}catch(i){if(this.logger.error("Failed to get current booking",{appointmentId:t,error:i}),o(i))throw i;throw n.networkError("Failed to get current booking",{error:i})}}async book(t,i,e){try{const o=await this.http.post(`/appointments/${t}/book`,{slotId:i,locationId:e??null});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(r){if(this.logger.error("Failed to book appointment",{appointmentId:t,slotId:i,locationId:e,error:r}),o(r))throw r;throw n.networkError("Failed to book appointment",{error:r})}}async cancel(t,i,e,r){try{await this.http.post(`/appointments/${t}/cancel`,{slotId:i,locationId:e??null,reason:r??null})}catch(s){if(this.logger.error("Failed to cancel appointment",{appointmentId:t,slotId:i,locationId:e,error:s}),o(s))throw s;throw n.networkError("Failed to cancel appointment",{error:s})}}async reschedule(t,i,e){try{const o=await this.http.post(`/appointments/${t}/reschedule`,{newSlotId:i,newLocationId:e??null}),n=a(u,o,`/appointments/${t}/reschedule`),r=await this.getMe(t);return r?{...r,confirmationCode:n.confirmationCode}:{bookingId:n.bookingId,appointmentId:t,slotId:n.newSlotId,locationId:e,startTime:"",endTime:"",confirmationCode:n.confirmationCode,consumerStatus:n.status}}catch(r){if(this.logger.error("Failed to reschedule appointment",{appointmentId:t,newSlotId:i,newLocationId:e,error:r}),o(r))throw r;throw n.networkError("Failed to reschedule appointment",{error:r})}}startMonitoring(t,o,n,i){if(this.destroyed)return;this.monitorRuntime.start(t,o,n),this.stopPolling(t);const e=(this.monitorGenerations.get(t)??0)+1;this.monitorGenerations.set(t,e);const s=r({baseMs:this.pollingIntervalMs,suppressBackoff:()=>!1,onDegradedChange:i,shouldContinue:()=>!this.destroyed&&this.monitorGenerations.get(t)===e&&this.pollSchedulers.has(t),run:async()=>{if(!this.destroyed&&this.monitorGenerations.get(t)===e)try{const o=await this.getMe(t);if(this.destroyed||this.monitorGenerations.get(t)!==e)return;const n=this.monitorRuntime.getDisplayAtom(t);if(!n)return;if(!o)return;if("cancelled"===o.consumerStatus||"no_show"===o.consumerStatus)return this.monitorRuntime.notify(t,{type:"ended",outcome:{type:o.consumerStatus}}),void this.stopMonitoring(t);const i=Math.max(0,Date.parse(o.startTime)-Date.now());n.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:i})}catch(o){throw this.logger.error("Appointment polling tick failed",{id:t,error:o}),o}}});this.pollSchedulers.set(t,s),s.start()}stopMonitoring(t){this.stopPolling(t),this.monitorRuntime.stop(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();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,7 +1,7 @@
|
|
|
1
1
|
import { AppointmentConsumerStatus, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
|
|
2
2
|
import { ReadableAtom } from 'nanostores';
|
|
3
3
|
import { AppointmentDisplayState } from '../experiences/distribution-monitor.types';
|
|
4
|
-
import { DistributionSummary
|
|
4
|
+
import { DistributionSummary } from '../experiences/journey.types';
|
|
5
5
|
/**
|
|
6
6
|
* Appointment booking facts for one consumer.
|
|
7
7
|
*
|
|
@@ -51,8 +51,6 @@ export interface AppointmentScheduled {
|
|
|
51
51
|
distribution: DistributionSummary & {
|
|
52
52
|
type: "appointment";
|
|
53
53
|
};
|
|
54
|
-
/** Scheduled-only waitlist attachment, cleared when the appointment opens. */
|
|
55
|
-
waitlist?: WaitlistAttachment;
|
|
56
54
|
}
|
|
57
55
|
/**
|
|
58
56
|
* Booking is open and this consumer has not booked an appointment.
|
|
@@ -123,8 +121,6 @@ export interface AppointmentScheduledView {
|
|
|
123
121
|
};
|
|
124
122
|
/** ISO 8601 opening timestamp chosen from the distribution start/open time. */
|
|
125
123
|
startsAt?: string;
|
|
126
|
-
/** Scheduled-only waitlist actions; this attachment never grants access. */
|
|
127
|
-
waitlist?: WaitlistAttachmentView;
|
|
128
124
|
}
|
|
129
125
|
/**
|
|
130
126
|
* Enterable appointment view rendered when the consumer can book a slot.
|
|
@@ -20,7 +20,7 @@ export interface AppointmentBooking {
|
|
|
20
20
|
startTime: string;
|
|
21
21
|
endTime: string;
|
|
22
22
|
confirmationCode?: string;
|
|
23
|
-
consumerStatus: "booked" | "checked_in" | "completed";
|
|
23
|
+
consumerStatus: "booked" | "checked_in" | "completed" | "cancelled" | "no_show";
|
|
24
24
|
}
|
|
25
25
|
export interface AppointmentModule {
|
|
26
26
|
get(appointmentId: string): Promise<import('../experiences/types').AppointmentDetails>;
|
|
@@ -33,7 +33,7 @@ export interface AppointmentModule {
|
|
|
33
33
|
book(appointmentId: string, slotId: string, locationId?: string): Promise<AppointmentBooking>;
|
|
34
34
|
cancel(appointmentId: string, slotId: string, locationId?: string, reason?: string): Promise<void>;
|
|
35
35
|
reschedule(appointmentId: string, newSlotId: string, newLocationId?: string): Promise<AppointmentBooking>;
|
|
36
|
-
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
|
|
36
|
+
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
|
|
37
37
|
stopMonitoring(id: string): void;
|
|
38
38
|
isMonitoring(id: string): boolean;
|
|
39
39
|
/**
|
|
@@ -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";function s(t){return!t||("lost"===t.type||"closed"===t.type)&&!t.reason}function o(t){return new u(t)}function a(e){const{distribution:n,consumer:o,grant:a,outcome:r}=e;if(!n)return{phase:"unavailable"};const c=function(t){if("auction"!==t.type)throw i.validationError("Auction driver received a non-auction distribution");return t}(n),u=o&&"auction"===o.mechanism?function(t){const{mechanism:i,...e}=t;return e}(o):d(n);if(a)return"completed"===r?.type?p(c,u,r):a.expiresAt&&Date.now()>new Date(a.expiresAt).getTime()?"open"===c.lifecycle?{phase:"enterable",mechanism:"auction",distribution:c,consumer:d(c)}:p(c,u,{type:"expired"}):{phase:"granted",mechanism:"auction",distribution:c,consumer:u,grant:a};if("closed"===c.lifecycle){if("not_bid"===u.status&&c.result===t.FLOOR_UNSOLD&&s(r))return p(c,u,{type:"lost",reason:t.FLOOR_UNSOLD});if("auction"===o?.mechanism&&!r)return p(c,u,{type:"lost"})}const h=r??function(t){return"lost"===t.status?{type:"lost"}:void 0}(u);return h?p(c,u,h):function(t){return"bidding"===t.status||"winning"===t.status||"outbid"===t.status}(u)?{phase:"participating",mechanism:"auction",distribution:c,consumer:u}:"scheduled"===c.lifecycle?{phase:"scheduled",mechanism:"auction",distribution:c}:"open"===c.lifecycle?{phase:"enterable",mechanism:"auction",distribution:c,consumer:u}:"settling"===c.lifecycle?{phase:"settling",mechanism:"auction",distribution:c,consumer:u}:p(c,u,{type:"closed"})}function r(t,i){return"auction"===t?.mechanism?{...t,status:"won"}:{mechanism:"auction",status:"won",id:i.id,distributionId:i.id}}function c(t,i,e){return t&&"auction"===t.mechanism&&i?"won"===i.type?{...t,status:"won"}:!e?.finalize&&"not_bid"===t.status&&s(i)?t:{...t,status:"lost"}:t}class u{constructor(t){this.deps=t,this.mechanism="auction"}project(t){return a(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(a({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 r=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===r)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(a({distribution:i.distribution,consumer:{mechanism:"auction",status:r,id:s,distributionId:s,currentBid:t,highestBid:o.highestBid}})),m.set({type:"auction",currentBid:t,highestBid:o.highestBid,bidCount:o.bidCount,status:r,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 d(t){return{status:"not_bid",distributionId:t.id}}function p(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{o as createAuctionDriver,s as isUpgradeableGenericLoss,a as projectAuctionSequence,r as withAuctionGrantedConsumer,c as withAuctionTerminalConsumer};
|
|
@@ -7,8 +7,11 @@ export declare class AuctionManagementModule implements AuctionModule {
|
|
|
7
7
|
private readonly events;
|
|
8
8
|
private readonly trackedParticipations;
|
|
9
9
|
private readonly inFlightRequests;
|
|
10
|
-
private readonly
|
|
10
|
+
private readonly pollSchedulers;
|
|
11
11
|
private readonly lastStatusCache;
|
|
12
|
+
private readonly hasSuccessfulReadThisSession;
|
|
13
|
+
private readonly lastSettleAtMs;
|
|
14
|
+
private readonly visibilityHandlers;
|
|
12
15
|
private readonly autoRebidConfigs;
|
|
13
16
|
private readonly monitorRuntime;
|
|
14
17
|
private readonly bidHistoryCache;
|
|
@@ -38,6 +41,7 @@ export declare class AuctionManagementModule implements AuctionModule {
|
|
|
38
41
|
private applyTransition;
|
|
39
42
|
private checkOutbidConditions;
|
|
40
43
|
private buildBidTransition;
|
|
44
|
+
private buildTerminalTransition;
|
|
41
45
|
private buildStatusTransition;
|
|
42
46
|
private buildMissingTransition;
|
|
43
47
|
get(auctionId: string): Promise<AuctionDetails>;
|
|
@@ -50,6 +54,10 @@ export declare class AuctionManagementModule implements AuctionModule {
|
|
|
50
54
|
private startWatching;
|
|
51
55
|
private stopWatching;
|
|
52
56
|
private stopAllWatching;
|
|
57
|
+
private shouldSuppressBackoff;
|
|
58
|
+
private isInSettlingWindow;
|
|
59
|
+
private registerVisibilityPoke;
|
|
60
|
+
private unregisterVisibilityPoke;
|
|
53
61
|
private bumpMonitorGeneration;
|
|
54
62
|
private getOperationGeneration;
|
|
55
63
|
private bumpOperationGeneration;
|
|
@@ -58,7 +66,7 @@ export declare class AuctionManagementModule implements AuctionModule {
|
|
|
58
66
|
disableAutoRebid(auctionId: string): void;
|
|
59
67
|
getAutoRebidConfig(auctionId: string): AutoRebidConfig | undefined;
|
|
60
68
|
private handleAutoRebid;
|
|
61
|
-
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void): void;
|
|
69
|
+
startMonitoring(id: string, context?: Record<string, unknown>, onUpdate?: (update: MonitorUpdate) => void, onDegradedChange?: (degraded: boolean) => void): void;
|
|
62
70
|
stopMonitoring(id: string): void;
|
|
63
71
|
isMonitoring(id: string): boolean;
|
|
64
72
|
destroy(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{AuctionBidderStateResponseSchema as t,AuctionHighestBidResponseSchema as i,AuctionBidHistoryResponseSchema as e}from"@fanfare-io/fanfare-sdk-contracts/auction";import{createError as n}from"../core/errors.js";import{getLogger as a}from"../core/logger.js";import{isMoneyString as s,addMoney as o,isMoneyGreaterThan as r,normalizeMoney as u,isMoneyLessThanOrEqualTo as d,subtractMoney as c}from"../core/money.js";import{parseResponse as h}from"../core/parse-response.js";import{DistributionMonitorRuntime as l}from"../experiences/distribution-monitor.runtime.js";import{getEventBus as g}from"../state/events.js";import{getSDKStore as p}from"../state/store.js";class m{constructor(t){this.logger=a(),this.events=g(),this.trackedParticipations=/* @__PURE__ */new Map,this.inFlightRequests=/* @__PURE__ */new Map,this.pollingIntervals=/* @__PURE__ */new Map,this.lastStatusCache=/* @__PURE__ */new Map,this.autoRebidConfigs=/* @__PURE__ */new Map,this.monitorRuntime=new l,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 p()}computeMinNextBid(t,i){if(i&&s(t)&&s(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:n,bidCount:a}){return{highestBid:e,currentBid:n,bidCount:a,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 n=e.currentBid,a=i.highestBid,o=t.highestBid||"0";if(s(n)&&s(a)&&s(o))return r(a,o)&&r(a,n)&&"winning"!==i.status}return"outbid"!==t.status&&"outbid"===i.status}buildBidTransition(t,i,e,n,a){const s="winning"===i.status?"winning":"outbid"===i.status?"outbid":"bidding";return{trackedParticipation:{auctionId:t,enteredAt:a?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:s,currentBid:e,highestBid:i.highestBid,bidCount:i.bidCount,lastBidAt:/* @__PURE__ */(new Date).toISOString(),metadata:a?.metadata,fingerprint:a?.fingerprint},displayState:this.buildDisplayState(n,{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:n,previousStatus:a}){const s=i.currentBid??n?.currentBid,o=i.bidCount??n?.bidCount;if("won"===i.terminal)return{trackedParticipation:null,displayState:this.buildDisplayState(e,{status:"won",highestBid:i.highestBid,currentBid:s,bidCount:o}),events:[{type:"auction:won",payload:{auctionId:t,winningBid:s??""}}],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:s,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:s,bidCount:o}),events:[],monitorUpdate:{type:"ended",outcome:{type:"closed",reason:"ended"}},stopWatching:!0,autoRebidStatus:null};const u={auctionId:t,enteredAt:n?.enteredAt??/* @__PURE__ */(new Date).toISOString(),status:i.status,currentBid:s,highestBid:i.highestBid,bidCount:o,metadata:n?.metadata,fingerprint:n?.fingerprint},d=[];let c=null;return a&&(this.checkOutbidConditions(a,i,n)&&(d.push({type:"auction:outbid",payload:{auctionId:t,yourBid:a.currentBid||n?.currentBid||"",highestBid:i.highestBid}}),c=i),"winning"!==a.status&&"winning"===i.status&&d.push({type:"auction:winning",payload:{auctionId:t,amount:s||"",highestBid:i.highestBid}}),a.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:s,bidCount:o}),events:d,monitorUpdate:null,stopWatching:!1,autoRebidStatus:c}}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,a,o){try{const r=this.lifecycleGeneration;if(!s(a))throw n.validationError("Invalid bid amount format. Must be a numeric string (e.g., '100.00')");if(!this.store.session)throw n.unauthorized("Not authenticated. Call auth.guest() or auth.login() first");let d=this.bumpOperationGeneration(e),c=this.getTrackedParticipation(e);c||(this.logger.warn("Attempting to bid without entering auction first",{auctionId:e}),await this.enter(e,void 0,o),c=this.getTrackedParticipation(e),d=this.bumpOperationGeneration(e)),this.logger.info("Placing bid",{auctionId:e,amount:a});const l=`/auctions/${e}/bid`,g=`/auctions/${e}/highest-bid`,[p,m,b]=await Promise.all([this.http.post(l,{amount:a},o),this.http.get(g),this.getAuctionDetailsCached(e)]),y=h(t,p,{endpoint:l},{throwOnFailure:!0});if(!y.ok)throw y.error;const f=h(i,m,{endpoint:g},{throwOnFailure:!0});if(!f.ok)throw f.error;const I=y.value,B=f.value,w=u(B.amount),v={status:"WINNING"===I.status||"WON"===I.status?"winning":"OUTBID"===I.status||"BIDDING"===I.status||"LOST"===I.status?"outbid":"accepted",amount:a,highestBid:w,bidCount:I.bidCount??(c?.bidCount||0)+1};return this.canApplyAsyncResult(e,r,void 0,d)?(this.applyTransition(e,this.buildBidTransition(e,v,a,b,c)),v):v}catch(r){throw this.logger.error("Failed to place bid",{auctionId:e,amount:a,error:r}),this.events.emit("auction:error",{auctionId:e,error:r}),r}}async enter(t,i,e){try{const a=this.lifecycleGeneration,s=this.store.session;if(!s)throw n.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,a,void 0,o))return;this.trackedParticipations.set(t,{auctionId:t,enteredAt:/* @__PURE__ */(new Date).toISOString(),status:"watching",metadata:i,fingerprint:s.deviceFingerprint}),this.emitTransitionEvents([{type:"auction:entered",payload:{auctionId:t}}])}catch(a){throw this.logger.error("Failed to enter auction",{auctionId:t,error:a}),this.events.emit("auction:error",{auctionId:t,error:a}),a}}async leave(t,i){try{const n=this.lifecycleGeneration,a=this.bumpOperationGeneration(t);this.logger.info("Leaving auction",{auctionId:t});try{await this.http.post(`/auctions/${t}/leave`,void 0,i)}catch(e){if(404!==e?.status)throw e;this.logger.debug("Leave endpoint not available",{auctionId:t})}if(!this.canApplyAsyncResult(t,n,void 0,a))return;this.stopMonitoring(t),this.trackedParticipations.delete(t),this.emitTransitionEvents([{type:"auction:left",payload:{auctionId:t}}])}catch(e){throw this.logger.error("Failed to leave auction",{auctionId:t,error:e}),this.events.emit("auction:error",{auctionId:t,error:e}),e}}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,n=this.bumpOperationGeneration(t),a=this.doStatus(t,e,void 0,n).finally(()=>{this.inFlightRequests.delete(t)});return this.inFlightRequests.set(t,a),a}async doStatus(e,n,a,s){try{const o=`/auctions/${e}/status`,r=`/auctions/${e}/highest-bid`,[d,c,l]=await Promise.all([this.http.get(o),this.http.get(r),this.getAuctionDetailsCached(e)]),g=h(t,d,{endpoint:o},{throwOnFailure:!0});if(!g.ok)throw g.error;const p=h(i,c,{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,I=u(b.amount),B=this.getTrackedParticipation(e),w=m.lastBidAmount??m.winningBidAmount;let v="watching";"WINNING"===m.status||"WON"===m.status?v="winning":"OUTBID"!==m.status&&"LOST"!==m.status&&"BIDDING"!==m.status||(v="outbid");const A="WON"===m.status?"won":"LOST"===m.status?"lost":void 0,C={auctionId:e,status:v,...A?{terminal:A}:{},currentBid:B?.currentBid??(w?u(w):void 0),highestBid:I,timeRemaining:Number.isFinite(f)?Math.max(0,f-Date.now()):0,bidCount:m.bidCount,admissionGrant:m.admissionGrant};if(!this.canApplyAsyncResult(e,n,a,s))return C;const S=this.buildStatusTransition(e,C,l,{previousParticipation:B,previousStatus:this.lastStatusCache.get(e)});return this.applyTransition(e,S),S.stopWatching?this.lastStatusCache.delete(e):this.lastStatusCache.set(e,C),this.events.emit("auction:status-updated",{auctionId:e,status:C}),C}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,n,a,s))return t;const i=this.buildMissingTransition();return this.applyTransition(e,i),this.lastStatusCache.delete(e),this.events.emit("auction:status-updated",{auctionId:e,status:t}),t}if(!this.canApplyAsyncResult(e,n,a,s))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 n=`/auctions/${t}/bids/history`,a=await this.http.get(n),s=h(e,a,{endpoint:n},{throwOnFailure:!0});if(!s.ok)throw s.error;const o=s.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){this.stopWatching(t);const e=this.bumpMonitorGeneration(t);this.logger.info("Starting auction status polling",{auctionId:t,intervalMs:i});const n=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,n).catch(i=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,n)&&this.logger.error("Initial status check failed",{auctionId:t,error:i})});const a=setInterval(()=>{const i=this.getOperationGeneration(t);this.doStatus(t,this.lifecycleGeneration,e,i).catch(n=>{this.canApplyAsyncResult(t,this.lifecycleGeneration,e,i)&&this.logger.error("Polling failed",{auctionId:t,error:n})})},i);this.pollingIntervals.set(t,a),this.events.emit("auction:watching-started",{auctionId:t,intervalMs:i})}stopWatching(t){this.bumpMonitorGeneration(t);const i=this.pollingIntervals.get(t);i&&(clearInterval(i),this.pollingIntervals.delete(t),this.lastStatusCache.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.pollingIntervals.keys()))this.stopWatching(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,n){return!this.destroyed&&this.lifecycleGeneration===i&&((void 0===e||this.monitorGenerations.get(t)===e)&&(void 0===n||this.getOperationGeneration(t)===n))}enableAutoRebid(t,i,e){if(!s(i)||!s(e))throw n.validationError("Invalid monetary format. Must be numeric strings (e.g., '100.00')");if(d(i,"0")||d(e,"0"))throw n.validationError("Max bid and increment must be positive");if(r(e,i))throw n.validationError("Increment cannot be greater than max bid");const a=this.getTrackedParticipation(t);if(!a)throw n.validationError("Must enter auction before enabling auto-rebid");const o=u(a.currentBid),h=c(i,o);this.autoRebidConfigs.set(t,{enabled:!0,maxBid:i,increment:e,remainingBudget:h,rebidCount:0}),this.logger.info("Auto-rebid enabled",{auctionId:t,maxBid:i,increment:e,remainingBudget:h}),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(!s(i.highestBid)||!s(e.maxBid)||!s(e.increment))return void this.logger.error("Invalid auto-rebid amounts",{auctionId:t,config:e});const n=o(i.highestBid,e.increment);if(r(n,e.maxBid))return this.logger.info("Auto-rebid would exceed max bid, disabling",{auctionId:t,wouldBid:n,maxBid:e.maxBid}),this.disableAutoRebid(t),void this.events.emit("auction:auto-rebid-max-reached",{auctionId:t,maxBid:e.maxBid,wouldNeedToBid:n});this.logger.info("Placing auto-rebid",{auctionId:t,amount:n});const a=await this.bid(t,n);e.lastRebidAt=/* @__PURE__ */(new Date).toISOString(),e.rebidCount=(e.rebidCount||0)+1,e.remainingBudget=c(e.maxBid,n),this.events.emit("auction:auto-rebid-placed",{auctionId:t,amount:n,remainingBudget:e.remainingBudget,status:a.status})}catch(n){this.logger.error("Auto-rebid failed",{auctionId:t,error:n}),this.events.emit("auction:auto-rebid-failed",{auctionId:t,error:n})}}startMonitoring(t,i,e){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)}stopMonitoring(t){this.stopWatching(t),this.monitorRuntime.stop(t)}isMonitoring(t){return this.monitorRuntime.has(t)||this.pollingIntervals.has(t)}destroy(){this.destroyed=!0,this.lifecycleGeneration+=1,this.stopAllWatching(),this.inFlightRequests.clear(),this.lastStatusCache.clear(),this.trackedParticipations.clear(),this.monitorGenerations.clear(),this.operationGenerations.clear(),this.bidHistoryCache.clear(),this.auctionDetailsCache.clear(),this.autoRebidConfigs.clear(),this.monitorRuntime.clear()}}export{m 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}}])}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};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AdmissionGrant, AuctionConsumerStatus, SequenceOutcome } from '@fanfare-io/fanfare-sdk-contracts/consumer-me';
|
|
2
2
|
import { ReadableAtom } from 'nanostores';
|
|
3
3
|
import { AuctionDisplayState } from '../experiences/distribution-monitor.types';
|
|
4
|
-
import { DistributionSummary
|
|
4
|
+
import { DistributionSummary } from '../experiences/journey.types';
|
|
5
5
|
/**
|
|
6
6
|
* Auction participation facts for one consumer.
|
|
7
7
|
*
|
|
@@ -50,8 +50,6 @@ export interface AuctionScheduled {
|
|
|
50
50
|
distribution: DistributionSummary & {
|
|
51
51
|
type: "auction";
|
|
52
52
|
};
|
|
53
|
-
/** Scheduled-only waitlist attachment, cleared when the auction opens. */
|
|
54
|
-
waitlist?: WaitlistAttachment;
|
|
55
53
|
}
|
|
56
54
|
/**
|
|
57
55
|
* Bidding is open and this consumer has not bid yet.
|
|
@@ -154,8 +152,6 @@ export interface AuctionScheduledView {
|
|
|
154
152
|
};
|
|
155
153
|
/** ISO 8601 opening timestamp chosen from the distribution start/open time. */
|
|
156
154
|
startsAt?: string;
|
|
157
|
-
/** Scheduled-only waitlist actions; this attachment never grants access. */
|
|
158
|
-
waitlist?: WaitlistAttachmentView;
|
|
159
155
|
}
|
|
160
156
|
/**
|
|
161
157
|
* Enterable auction view rendered when the first bid can be placed.
|
|
@@ -184,7 +180,8 @@ export interface AuctionEnterableView {
|
|
|
184
180
|
* Participating auction view rendered while the consumer is bidding.
|
|
185
181
|
*/
|
|
186
182
|
export interface AuctionParticipatingView {
|
|
187
|
-
/** Phase discriminator; `bid()`
|
|
183
|
+
/** Phase discriminator; `bid()` is legal on this view. Leave is not offered: a
|
|
184
|
+
* placed bid is a binding commitment (see auction.driver participating view). */
|
|
188
185
|
phase: "participating";
|
|
189
186
|
/** Mechanism discriminator for auction rendering. */
|
|
190
187
|
mechanism: "auction";
|
|
@@ -200,13 +197,6 @@ export interface AuctionParticipatingView {
|
|
|
200
197
|
* closed, or the server rejects the bid.
|
|
201
198
|
*/
|
|
202
199
|
bid(amount: string): Promise<void>;
|
|
203
|
-
/**
|
|
204
|
-
* Leave the auction participation.
|
|
205
|
-
*
|
|
206
|
-
* Legal only while this view is current. The promise rejects if the consumer
|
|
207
|
-
* is no longer participating or the server rejects the leave request.
|
|
208
|
-
*/
|
|
209
|
-
leave(): Promise<void>;
|
|
210
200
|
}
|
|
211
201
|
/**
|
|
212
202
|
* Settling auction view rendered while the server computes the auction result.
|
|
@@ -240,7 +230,7 @@ export interface AuctionGrantedView {
|
|
|
240
230
|
/** Admission credential issued by the server for this auction win. */
|
|
241
231
|
grant: AdmissionGrant;
|
|
242
232
|
/**
|
|
243
|
-
* Claim the auction admission credential for
|
|
233
|
+
* Claim the auction admission credential for checkout.
|
|
244
234
|
*
|
|
245
235
|
* Legal only while this view is current. It returns the credential
|
|
246
236
|
* synchronously and may throw if the journey has already left the granted
|
|
@@ -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
|
*
|