@fanfare-io/fanfare-sdk-core 0.10.0 → 0.12.0

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