@fanfare-io/fanfare-sdk-core 0.7.2 → 0.8.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.
@@ -8,7 +8,17 @@ export declare class AuthenticationModule implements AuthModule {
8
8
  private events;
9
9
  private refreshTimer?;
10
10
  private refreshPromise?;
11
+ private authChannel?;
12
+ private lastBroadcastTokens;
11
13
  constructor(http: HttpClient, beaconHttp: HttpClient);
14
+ /**
15
+ * Subscribe to the cross-tab auth channel so this tab caches the bearer a refresh
16
+ * winner in another tab broadcasts. Best-effort: when BroadcastChannel is
17
+ * unavailable a queued caller falls back to the reactive-401 path instead.
18
+ */
19
+ private setupTokenChannel;
20
+ /** Hand the freshly-minted bearer to other tabs over the ephemeral auth channel. */
21
+ private broadcastSessionTokens;
12
22
  /**
13
23
  * Check current authentication status
14
24
  */
@@ -41,7 +51,11 @@ export declare class AuthenticationModule implements AuthModule {
41
51
  */
42
52
  logout(): Promise<void>;
43
53
  /**
44
- * Clear all local auth state: store, access-token header, and beacon-token header.
54
+ * Clear all local auth state: store, access-token header, beacon-token header,
55
+ * and the durably-persisted refresh token. Clearing the refresh token ensures a
56
+ * forced re-auth does not leave a stale token that a reload or another tab would
57
+ * present; the access/beacon bearer is memory-only, so there is nothing to clear
58
+ * from durable storage — only the in-memory broadcast cache.
45
59
  */
46
60
  private clearAuthState;
47
61
  /**
@@ -49,11 +63,77 @@ export declare class AuthenticationModule implements AuthModule {
49
63
  */
50
64
  getSession(): Session | null;
51
65
  /**
52
- * Refresh the access token
66
+ * Rehydrate the durable refresh token after a reload and restore a working
67
+ * bearer. The session is rehydrated from localStorage by the store; the refresh
68
+ * token lives in IndexedDB (never localStorage), so it is restored here. The
69
+ * access token is intentionally NOT persisted, so after a reload there is no
70
+ * bearer on the fresh HTTP client — and scheduling alone would leave the FIRST
71
+ * authenticated call tokenless until the timer fires minutes later (a 401 that
72
+ * can spuriously log the user out). So when a refresh token is restored but no
73
+ * bearer is installed, refresh eagerly here (awaited before init returns the SDK,
74
+ * so no race with the first call) to mint and install an access token. The
75
+ * eager refresh reuses the single-flight Web Lock and publishes cross-tab. A
76
+ * no-op when no token is persisted; in steady-state (bearer already present) it
77
+ * only resumes proactive scheduling. A failed eager refresh is swallowed: the
78
+ * reactive 401 path still re-drives re-authentication.
79
+ */
80
+ rehydrate(): Promise<void>;
81
+ /** Whether the HTTP client currently carries an `Authorization` bearer header. */
82
+ private hasBearer;
83
+ /**
84
+ * Refresh the access token.
85
+ *
86
+ * Single-flight is enforced cross-tab via `navigator.locks` when available:
87
+ * the rotated refresh token is single-use, so two callers that race would
88
+ * otherwise consume each other's token. The token observed BEFORE acquiring the
89
+ * lock is the "entry token"; once inside the lock `_doRefresh` re-reads
90
+ * IndexedDB and — if the token already changed (a concurrent caller rotated it)
91
+ * — skips the POST and adopts the now-current session instead of POSTing a
92
+ * spent token (which would 401). When `navigator.locks` is unavailable, fall
93
+ * back to the in-process `refreshPromise` (same-tab dedupe only).
53
94
  */
54
95
  refresh(): Promise<void>;
55
96
  /**
56
- * Internal refresh implementation
97
+ * The absolute URL a DPoP `htu` is signed against for an auth endpoint. Built
98
+ * the same way the HTTP client resolves request URLs (`new URL(path, baseUrl)`),
99
+ * so an absolute path discards the base pathname — matching the htu the server
100
+ * reconstructs from its public base URL.
101
+ */
102
+ private absoluteAuthUrl;
103
+ /**
104
+ * Build best-effort DPoP proof headers for a session-minting POST (guest /
105
+ * otp-verify / external-exchange). Unlike `/auth/refresh`, these are best-effort
106
+ * on both ends: when a proof is built the server binds the session to the
107
+ * verified key (so a later refresh works); when it cannot (no crypto / non-secure
108
+ * context) the headers are simply omitted and the server mints an unbound
109
+ * session (back-compat). Returns `{}` when no proof is available.
110
+ */
111
+ private sessionMintProofHeaders;
112
+ /**
113
+ * Set the refresh token in the in-memory store AND mirror it to its durable
114
+ * IndexedDB home (or clear that home on null), so a reload or another tab can
115
+ * refresh. IndexedDB is best-effort: a failed persist still leaves the
116
+ * in-memory token usable for the current session (Stage-0-like behavior).
117
+ */
118
+ private setRefreshTokenDurable;
119
+ /**
120
+ * Apply a successful refresh's bearer material to this tab: install the new
121
+ * access token on the HTTP client, the new beacon token on the beacon client +
122
+ * store, resume proactive scheduling, and emit `auth:refreshed`. Shared by the
123
+ * tab that performed the refresh and by a queued cross-tab caller adopting the
124
+ * winner's published tokens, so both end in the identical post-refresh state.
125
+ */
126
+ private applySessionTokens;
127
+ /**
128
+ * Internal refresh implementation. The refresh token is durably persisted in
129
+ * IndexedDB, so it survives a reload. The token is re-read from IndexedDB here
130
+ * (inside the cross-tab lock) because another caller may have rotated it; the
131
+ * in-memory store is reconciled to whatever IndexedDB holds.
132
+ *
133
+ * `entryToken` is the token observed before the lock was acquired (locked path
134
+ * only). If IndexedDB now holds a DIFFERENT token, a concurrent caller already
135
+ * refreshed while this one queued — so adopt the rotated session and skip the
136
+ * POST rather than spend a single-use token that is already consumed.
57
137
  */
58
138
  private _doRefresh;
59
139
  /**
@@ -62,10 +142,10 @@ export declare class AuthenticationModule implements AuthModule {
62
142
  private handleUnauthorized;
63
143
  /**
64
144
  * Schedule a proactive token refresh ahead of expiry so an active session
65
- * rotates before a request hits a 401. Scheduled only while a refresh token is
66
- * held in memory: after a reload the in-memory token is gone, so there is
67
- * nothing to present and refresh is driven reactively (a 401 drives
68
- * re-authentication) rather than by a timer that could only clear auth state.
145
+ * rotates before a request hits a 401. Scheduled whenever a refresh token is
146
+ * available: the token is durably persisted in IndexedDB and rehydrated into the
147
+ * store on init, so after a reload there IS a token to present and scheduling
148
+ * resumes restoring seamless background rotation.
69
149
  */
70
150
  private scheduleTokenRefresh;
71
151
  /**
@@ -1 +1 @@
1
- import{getLogger as e}from"../core/logger.js";import{getEventBus as t}from"../state/events.js";import{getSDKStore as s}from"../state/store.js";class r{constructor(r,o){this.logger=e(),this.store=s(),this.events=t(),this.http=r,this.beaconHttp=o,this.http.updateConfig({...this.http.getConfig(),onUnauthorized:async()=>{await this.handleUnauthorized()}});const i=this.store.beaconToken;i&&this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":i}})}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{session:e,accessToken:t,refreshToken:s,beaconToken:r}=await this.http.post("/auth/guest");return this.store.setSession(e),this.store.setRefreshToken(s??null),this.store.setBeaconToken(r),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${t}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":r}}),this.scheduleTokenRefresh(e.expiresAt),this.events.emit("auth:authenticated",{session:e,isNew:!0}),e}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{session:s,accessToken:r,refreshToken:o,beaconToken:i}=await this.http.post("/auth/otp/verify",t);return this.store.setSession(s),this.store.setRefreshToken(o??null),this.store.setBeaconToken(i),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${r}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":i}}),this.scheduleTokenRefresh(s.expiresAt),this.events.emit("auth:authenticated",{session:s,isNew:!0}),s}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{session:s,accessToken:r,refreshToken:o,beaconToken:i}=await this.http.post("/auth/external/exchange",t);return this.store.setSession(s),this.store.setRefreshToken(o??null),this.store.setBeaconToken(i),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${r}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":i}}),this.scheduleTokenRefresh(s.expiresAt),this.events.emit("auth:authenticated",{session:s,isNew:!0}),s}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 this.http.post("/auth/logout",e?{refreshToken:e}:{},{skipUnauthorizedHandler:!0}).catch(()=>{}),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(),t}}clearAuthState(){this.cancelTokenRefresh(),this.store.clearAll();const e=this.http.getConfig(),{Authorization:t,...s}=e.headers||{};this.http.updateConfig({...e,headers:s});const r=this.beaconHttp.getConfig(),{"X-Beacon-Token":o,...i}=r.headers||{};this.beaconHttp.updateConfig({...r,headers:i})}getSession(){return this.store.session}async refresh(){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)}async _doRefresh(){const e=this.store.refreshToken;if(!e)throw this.logger.info("No refresh token available; clearing auth state"),this.clearAuthState(),this.events.emit("auth:logout",{reason:"expired"}),new Error("No refresh token available");try{this.logger.info("Refreshing access token");const{accessToken:t,refreshToken:s,beaconToken:r}=await this.http.post("/auth/refresh",{refreshToken:e});this.store.setRefreshToken(s??null),this.store.setBeaconToken(r),this.http.updateConfig({...this.http.getConfig(),headers:{...this.http.getConfig().headers,Authorization:`Bearer ${t}`}}),this.beaconHttp.updateConfig({...this.beaconHttp.getConfig(),headers:{...this.beaconHttp.getConfig().headers,"X-Beacon-Token":r}});const o=this.store.session;o&&o.expiresAt&&this.scheduleTokenRefresh(o.expiresAt),this.store.session&&this.events.emit("auth:refreshed",{session:this.store.session})}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.logger.error("Failed to refresh token",e),await this.logout(),e}}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()-3e5,r=s-t;r<=0?this.refresh().catch(e=>{this.logger.error("Failed to refresh expired token",e)}):(this.logger.debug("Scheduling token refresh",{expiresAt:new Date(e).toISOString(),refreshAt:new Date(s).toISOString(),delayMs:r}),this.refreshTimer=setTimeout(()=>{this.refresh().catch(e=>{this.logger.error("Scheduled token refresh failed",e)})},r))}cancelTokenRefresh(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0,this.logger.debug("Cancelled token refresh timer"))}destroy(){this.cancelTokenRefresh(),this.refreshPromise=void 0}}export{r 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 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 +1 @@
1
- import{createError as i}from"../core/errors.js";const n={production:"https://consumer.fanfare.io/api",development:"http://localhost:4802"},o={production:"https://beacon.fanfare.io",development:"http://localhost:4803"},t=/* @__PURE__ */new Set(["https://consumer.fanfare.io"]),e=/* @__PURE__ */new Set(["https://beacon.fanfare.io"]),r={environment:"production",auth:{persistSession:!0,sessionDuration:3600},logging:{level:"error"}};class a{constructor(n){if(this.config={organizationId:n.organizationId,publishableKey:n.publishableKey,environment:n.environment??r.environment??"production",apiUrl:n.apiUrl,credentials:n.credentials??"include",debug:n.debug??!1,auth:{...r.auth,...n.auth},logging:{...r.logging,...n.logging},sync:n.sync,beacon:n.beacon,features:{fingerprinting:n.features?.fingerprinting??!0}},!this.config.organizationId)throw i.invalidConfig("organizationId is required");if(!this.config.publishableKey)throw i.invalidConfig("publishableKey is required");"production"===this.config.environment&&(s("apiUrl",this.apiUrl,t),s("beacon.baseUrl",this.beaconUrl,e))}get apiUrl(){return this.config.apiUrl||n[this.config.environment]}get beaconUrl(){return this.config.beacon?.baseUrl||o[this.config.environment]}get(){return this.config}getValue(i){return this.config[i]}}function s(n,o,t){let e;try{e=new URL(o)}catch{throw i.invalidConfig(`${n} must be a valid URL in production`)}if("https:"!==e.protocol)throw i.invalidConfig(`${n} must use HTTPS in production`);if(!t.has(e.origin))throw i.invalidConfig(`${n} must use a trusted Fanfare destination in production`)}export{n as API_URLS,o as BEACON_URLS,a as Config,r as DEFAULT_CONFIG};
1
+ import{createError as i}from"../core/errors.js";const n={production:"https://consumer.fanfare.io/api",development:"http://localhost:4802"},e={production:"https://beacon.fanfare.io",development:"http://localhost:4803"},o=/* @__PURE__ */new Set(["https://consumer.fanfare.io"]),t=/* @__PURE__ */new Set(["https://beacon.fanfare.io"]),r={environment:"production",auth:{persistSession:!0,sessionDuration:3600},logging:{level:"error"}};class a{constructor(n){if(this.config={organizationId:n.organizationId,publishableKey:n.publishableKey,environment:n.environment??r.environment??"production",apiUrl:n.apiUrl,credentials:n.credentials??"include",debug:n.debug??!1,autoResume:n.autoResume??!0,auth:{...r.auth,...n.auth},logging:{...r.logging,...n.logging},sync:n.sync,beacon:n.beacon,features:{fingerprinting:n.features?.fingerprinting??!0}},!this.config.organizationId)throw i.invalidConfig("organizationId is required");if(!this.config.publishableKey)throw i.invalidConfig("publishableKey is required");"production"===this.config.environment&&(s("apiUrl",this.apiUrl,o),s("beacon.baseUrl",this.beaconUrl,t))}get apiUrl(){return this.config.apiUrl||n[this.config.environment]}get beaconUrl(){return this.config.beacon?.baseUrl||e[this.config.environment]}get(){return this.config}getValue(i){return this.config[i]}}function s(n,e,o){let t;try{t=new URL(e)}catch{throw i.invalidConfig(`${n} must be a valid URL in production`)}if("https:"!==t.protocol)throw i.invalidConfig(`${n} must use HTTPS in production`);if(!o.has(t.origin))throw i.invalidConfig(`${n} must use a trusted Fanfare destination in production`)}export{n as API_URLS,e as BEACON_URLS,a as Config,r as DEFAULT_CONFIG};
@@ -1,2 +1,2 @@
1
- import{AppointmentManagementModule as e}from"../appointments/appointment.module.js";import{AuctionManagementModule as t}from"../auctions/auction.module.js";import{AuthenticationModule as r}from"../auth/auth.module.js";import{BeaconTrackingModule as n}from"../beacon/beacon.module.js";import{ChallengeManagementModule as i}from"../challenges/challenge.module.js";import{Config as o}from"../config/index.js";import{DrawManagementModule as s}from"../draws/draw.module.js";import{ExperienceManagementModule as a}from"../experiences/experience.module.js";import{ExperienceJourney as d}from"../experiences/journey.js";import{QueueManagementModule as l}from"../queues/queue.module.js";import{resolveCapabilityOptions as u}from"../state/capability-token-registry.js";import{getEventBus as c}from"../state/events.js";import{getSDKStore as g}from"../state/store.js";import{CrossTabCoordinator as y}from"../sync/cross-tab-coordinator.js";import{TimedReleaseManagementModule as p}from"../timed-releases/timed-release.module.js";import{version as f}from"../version.js";import{WaitlistManagementModule as m}from"../waitlists/waitlist.module.js";import{FanfareError as b,ErrorCodes as w}from"./errors.js";import{createHttpClient as v}from"./http.js";import{createLogger as M}from"./logger.js";let h=null;async function I(I){const S=new o(I),j=S.get(),A=M({enabled:j.debug||!1,level:j.debug?"debug":"warn",prefix:"Fanfare"});if(A.info("Initializing Fanfare SDK",{organizationId:I.organizationId,environment:j.environment}),h)throw new b("Fanfare SDK is already initialized. The SDK uses a shared event bus and store; multiple concurrent instances will corrupt state and analytics. Call destroy() on the existing instance before initializing a new one.",w.ALREADY_INITIALIZED);const k=/* @__PURE__ */Symbol("fanfare-sdk"),x=v({baseUrl:S.apiUrl,credentials:j.credentials,headers:{"X-Organization-Id":I.organizationId,"X-Publishable-Key":I.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":f,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},j),D=v({baseUrl:S.beaconUrl,credentials:"omit",headers:{"X-Organization-Id":I.organizationId,"X-Publishable-Key":I.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":f,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},j),E=new r(x,D),q=new i(x),C=new l(x),R=new s(x),z=new t(x),O=new e(x),F=new m(x),K=new p(x),X=new a(x,C,R,z,F,K),P=new n(D,j.beacon||{}),T=c(),J=g(),$=/* @__PURE__ */new Map,N=/* @__PURE__ */new Map,V=/* @__PURE__ */new Map;let B,U;const L=e=>{const t=N.get(e);if(t)return A.debug("Returning existing journey handle",{experienceId:e}),t;A.info("Creating new journey",{experienceId:e});const r=new d(e,B);$.set(e,r),U?.registerJourney(r);const n={view$:r.view$,events$:r.events$,latestEvent$:r.latestEvent$,snapshot$:r.state,ackEvent:e=>r.ackEvent(e),ackAllEvents:()=>r.ackAllEvents(),destroy:()=>{r.destroy(),U?.unregisterJourney(e),$.delete(e),N.delete(e)}};return N.set(e,n),n},G=async e=>{const t=e??await X.getMe(),r=/* @__PURE__ */new Set,n=new Set(t.active.journeys.map(e=>e.experienceId));for(const i of n)L(i);for(const i of n){const e=$.get(i);e&&await e.resumeFromMe(t)&&r.add(i)}return[...r]},H={check:()=>E.check(),guest:()=>E.guest(),requestOtp:e=>E.requestOtp(e),verifyOtp:e=>E.verifyOtp(e),exchangeExternal:e=>E.exchangeExternal(e),login:e=>E.login(e),logout:()=>E.logout(),getSession:()=>E.getSession(),refresh:()=>E.refresh()},_={initiate:e=>q.initiate(e),verify:e=>q.verify(e)},Y={get:e=>L(e),list:()=>Array.from(
2
- /* @__PURE__ */new Set([...N.keys(),...$.keys(),...Object.keys(J.activeJourneys)])),resumeAll:e=>(async e=>{const t=/* @__PURE__ */new Set;for(const r of Object.keys(J.activeJourneys))L(r),t.add(r);if(!e&&!E.getSession())return[...t];for(const r of await G(e))t.add(r);return[...t]})(e)},Z={track:e=>P.track(e),trackBatch:e=>P.trackBatch(e),flush:()=>P.flush()},Q=(e,t)=>{if(V.has(e.id))A.warn("CDP adapter already registered, skipping",{adapterId:e.id});else{try{e.init(T,t)}catch(r){return void A.error("CDP adapter failed to initialize",{adapterId:e.id,error:r})}V.set(e.id,e),A.info("CDP adapter registered",{adapterId:e.id})}},W=(e,t)=>T.on(e,t),ee=async()=>{A.info("Destroying SDK instance");for(const t of V.values())try{t.destroy()}catch(e){A.error("CDP adapter failed to destroy",{adapterId:t.id,error:e})}V.clear();for(const t of Array.from(N.values()))try{t.destroy()}catch(e){A.error("Journey handle failed to destroy",{error:e})}$.clear(),N.clear(),"destroy"in E&&"function"==typeof E.destroy&&E.destroy(),C.destroy(),R.destroy(),z.destroy(),O.destroy(),F.destroy(),K.destroy(),X.destroy(),P.destroy(),U?.close(),T.off(),h===k&&(h=null)};B={auth:H,challenges:_,journeys:Y,queues:{get:e=>C.get(e),enter:(e,t,r)=>C.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>C.leave(e,u({distributionId:e},t)),status:e=>C.status(e),startMonitoring:(e,t,r)=>C.startMonitoring(e,t,r),stopMonitoring:e=>C.stopMonitoring(e),isMonitoring:e=>C.isMonitoring(e)},draws:{get:e=>R.get(e),enter:(e,t,r)=>R.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>R.leave(e,u({distributionId:e},t)),status:e=>R.status(e),startMonitoring:(e,t,r)=>R.startMonitoring(e,t,r),stopMonitoring:e=>R.stopMonitoring(e),isMonitoring:e=>R.isMonitoring(e)},auctions:{get:e=>z.get(e),bid:(e,t,r)=>z.bid(e,t,u({distributionId:e},r)),enter:(e,t,r)=>z.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>z.leave(e,u({distributionId:e},t)),status:e=>z.status(e),getBidHistory:e=>z.getBidHistory(e),enableAutoRebid:(e,t,r)=>z.enableAutoRebid(e,t,r),disableAutoRebid:e=>z.disableAutoRebid(e),getAutoRebidConfig:e=>z.getAutoRebidConfig(e),startMonitoring:(e,t,r)=>z.startMonitoring(e,t,r),stopMonitoring:e=>z.stopMonitoring(e),isMonitoring:e=>z.isMonitoring(e),destroy:()=>z.destroy()},experiences:{get:e=>X.get(e),enter:e=>X.enter(e),leave:e=>X.leave(e),getMe:()=>X.getMe(),findSequence:(e,t)=>X.findSequence(e,t),validateSequenceAccess:(e,t)=>X.validateSequenceAccess(e,t),selectSequence:e=>X.selectSequence(e),getCurrentDistributions:e=>X.getCurrentDistributions(e),enterDistribution:e=>{if(!e)throw new b("Distribution is required",w.VALIDATION_ERROR);return X.enterDistribution(e)},getActiveSession:()=>X.getActiveSession(),isInExperience:e=>X.isInExperience(e),getSelectedSequence:()=>X.getSelectedSequence(),createJourney:e=>L(e),resumeJourneysFromMe:e=>G(e),destroy:()=>X.destroy()},waitlists:{enter:(e,t)=>F.enter(e,u({distributionId:e},t)),leave:(e,t)=>F.leave(e,u({distributionId:e},t)),getStatus:e=>F.getStatus(e),destroy:()=>F.destroy()},timedReleases:{get:e=>K.get(e),enter:(e,t,r)=>K.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>K.leave(e,u({distributionId:e},t)),complete:e=>K.complete(e),status:e=>K.status(e),startMonitoring:(e,t,r)=>K.startMonitoring(e,t,r),stopMonitoring:e=>K.stopMonitoring(e),isMonitoring:e=>K.isMonitoring(e)},appointments:{get:e=>O.get(e),getSlots:(e,t)=>O.getSlots(e,t),getMe:e=>O.getMe(e),book:(e,t,r)=>O.book(e,t,r),cancel:(e,t,r,n)=>O.cancel(e,t,r,n),reschedule:(e,t,r)=>O.reschedule(e,t,r),startMonitoring:(e,t,r)=>O.startMonitoring(e,t,r),stopMonitoring:e=>O.stopMonitoring(e),isMonitoring:e=>O.isMonitoring(e),bumpMonitorGeneration:e=>O.bumpMonitorGeneration(e),destroy:()=>O.destroy()},beacon:Z,use:Q,on:W,destroy:ee};const te=!1!==I.sync&&"object"==typeof I.sync?I.sync:{};U=new y({enabled:!1!==I.sync&&!1!==te.enabled,channelName:te.channelName}),A.info("Inter-tab sync initialized",{tabId:U.getTabId(),enabled:U.isEnabled()});const re={auth:H,challenges:_,journeys:Y,beacon:Z,appointments:B.appointments,use:Q,on:W,destroy:ee};return h=k,A.info("SDK initialized successfully"),re}export{I as init};
1
+ import{AppointmentManagementModule as e}from"../appointments/appointment.module.js";import{AuctionManagementModule as t}from"../auctions/auction.module.js";import{AuthenticationModule as r}from"../auth/auth.module.js";import{BeaconTrackingModule as n}from"../beacon/beacon.module.js";import{ChallengeManagementModule as i}from"../challenges/challenge.module.js";import{Config as o}from"../config/index.js";import{DrawManagementModule as s}from"../draws/draw.module.js";import{ExperienceManagementModule as a}from"../experiences/experience.module.js";import{ExperienceJourney as d}from"../experiences/journey.js";import{QueueManagementModule as l}from"../queues/queue.module.js";import{resolveCapabilityOptions as u}from"../state/capability-token-registry.js";import{getEventBus as c}from"../state/events.js";import{getSDKStore as g}from"../state/store.js";import{CrossTabCoordinator as y}from"../sync/cross-tab-coordinator.js";import{TimedReleaseManagementModule as m}from"../timed-releases/timed-release.module.js";import{version as f}from"../version.js";import{WaitlistManagementModule as p}from"../waitlists/waitlist.module.js";import{FanfareError as b,ErrorCodes as w}from"./errors.js";import{createHttpClient as h}from"./http.js";import{createLogger as v}from"./logger.js";let M=null;async function I(I){const S=new o(I),j=S.get(),A=v({enabled:j.debug||!1,level:j.debug?"debug":"warn",prefix:"Fanfare"});if(A.info("Initializing Fanfare SDK",{organizationId:I.organizationId,environment:j.environment}),M)throw new b("Fanfare SDK is already initialized. The SDK uses a shared event bus and store; multiple concurrent instances will corrupt state and analytics. Call destroy() on the existing instance before initializing a new one.",w.ALREADY_INITIALIZED);const k=/* @__PURE__ */Symbol("fanfare-sdk"),x=h({baseUrl:S.apiUrl,credentials:j.credentials,headers:{"X-Organization-Id":I.organizationId,"X-Publishable-Key":I.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":f,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},j),D=h({baseUrl:S.beaconUrl,credentials:"omit",headers:{"X-Organization-Id":I.organizationId,"X-Publishable-Key":I.publishableKey,"X-Fanfare-API-Version":"2025-01-15","X-Fanfare-SDK-Version":f,"X-Fanfare-Client-Type":"browser"},timeout:3e4,retryConfig:{maxRetries:3,delayTimer:1e3,retryOnNetworkError:!0}},j),E=new r(x,D),R=new i(x),q=new l(x),C=new s(x),z=new t(x),O=new e(x),F=new p(x),K=new m(x),X=new a(x,q,C,z,F,K),P=new n(D,j.beacon||{}),T=c(),J=g(),$=/* @__PURE__ */new Map,N=/* @__PURE__ */new Map,V=/* @__PURE__ */new Map;let B,U;const L=e=>{const t=N.get(e);if(t)return A.debug("Returning existing journey handle",{experienceId:e}),t;A.info("Creating new journey",{experienceId:e});const r=new d(e,B);$.set(e,r),U?.registerJourney(r);const n={view$:r.view$,events$:r.events$,latestEvent$:r.latestEvent$,snapshot$:r.state,ackEvent:e=>r.ackEvent(e),ackAllEvents:()=>r.ackAllEvents(),destroy:()=>{r.destroy(),U?.unregisterJourney(e),$.delete(e),N.delete(e)}};return N.set(e,n),n},G=async e=>{const t=e??await X.getMe(),r=/* @__PURE__ */new Set,n=new Set(t.active.journeys.map(e=>e.experienceId));for(const i of n)L(i);for(const i of n){const e=$.get(i);e&&await e.resumeFromMe(t)&&r.add(i)}return[...r]},H=async e=>{const t=/* @__PURE__ */new Set;for(const r of Object.keys(J.activeJourneys))L(r),t.add(r);if(!e&&!E.getSession())return[...t];for(const r of await G(e))t.add(r);return[...t]},_={check:()=>E.check(),guest:()=>E.guest(),requestOtp:e=>E.requestOtp(e),verifyOtp:e=>E.verifyOtp(e),exchangeExternal:e=>E.exchangeExternal(e),login:e=>E.login(e),logout:()=>E.logout(),getSession:()=>E.getSession(),refresh:()=>E.refresh()},Y={initiate:e=>R.initiate(e),verify:e=>R.verify(e)},Z={get:e=>L(e),list:()=>Array.from(
2
+ /* @__PURE__ */new Set([...N.keys(),...$.keys(),...Object.keys(J.activeJourneys)])),resumeAll:e=>H(e)},Q={track:e=>P.track(e),trackBatch:e=>P.trackBatch(e),flush:()=>P.flush()},W=(e,t)=>{if(V.has(e.id))A.warn("CDP adapter already registered, skipping",{adapterId:e.id});else{try{e.init(T,t)}catch(r){return void A.error("CDP adapter failed to initialize",{adapterId:e.id,error:r})}V.set(e.id,e),A.info("CDP adapter registered",{adapterId:e.id})}},ee=(e,t)=>T.on(e,t),te=async()=>{A.info("Destroying SDK instance");for(const t of V.values())try{t.destroy()}catch(e){A.error("CDP adapter failed to destroy",{adapterId:t.id,error:e})}V.clear();for(const t of Array.from(N.values()))try{t.destroy()}catch(e){A.error("Journey handle failed to destroy",{error:e})}$.clear(),N.clear(),"destroy"in E&&"function"==typeof E.destroy&&E.destroy(),q.destroy(),C.destroy(),z.destroy(),O.destroy(),F.destroy(),K.destroy(),X.destroy(),P.destroy(),U?.close(),T.off(),M===k&&(M=null)};B={auth:_,challenges:Y,journeys:Z,queues:{get:e=>q.get(e),enter:(e,t,r)=>q.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>q.leave(e,u({distributionId:e},t)),status:e=>q.status(e),startMonitoring:(e,t,r)=>q.startMonitoring(e,t,r),stopMonitoring:e=>q.stopMonitoring(e),isMonitoring:e=>q.isMonitoring(e)},draws:{get:e=>C.get(e),enter:(e,t,r)=>C.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>C.leave(e,u({distributionId:e},t)),status:e=>C.status(e),startMonitoring:(e,t,r)=>C.startMonitoring(e,t,r),stopMonitoring:e=>C.stopMonitoring(e),isMonitoring:e=>C.isMonitoring(e)},auctions:{get:e=>z.get(e),bid:(e,t,r)=>z.bid(e,t,u({distributionId:e},r)),enter:(e,t,r)=>z.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>z.leave(e,u({distributionId:e},t)),status:e=>z.status(e),getBidHistory:e=>z.getBidHistory(e),enableAutoRebid:(e,t,r)=>z.enableAutoRebid(e,t,r),disableAutoRebid:e=>z.disableAutoRebid(e),getAutoRebidConfig:e=>z.getAutoRebidConfig(e),startMonitoring:(e,t,r)=>z.startMonitoring(e,t,r),stopMonitoring:e=>z.stopMonitoring(e),isMonitoring:e=>z.isMonitoring(e),destroy:()=>z.destroy()},experiences:{get:e=>X.get(e),enter:e=>X.enter(e),leave:e=>X.leave(e),getMe:()=>X.getMe(),findSequence:(e,t)=>X.findSequence(e,t),validateSequenceAccess:(e,t)=>X.validateSequenceAccess(e,t),selectSequence:e=>X.selectSequence(e),getCurrentDistributions:e=>X.getCurrentDistributions(e),enterDistribution:e=>{if(!e)throw new b("Distribution is required",w.VALIDATION_ERROR);return X.enterDistribution(e)},getActiveSession:()=>X.getActiveSession(),isInExperience:e=>X.isInExperience(e),getSelectedSequence:()=>X.getSelectedSequence(),createJourney:e=>L(e),resumeJourneysFromMe:e=>G(e),destroy:()=>X.destroy()},waitlists:{enter:(e,t)=>F.enter(e,u({distributionId:e},t)),leave:(e,t)=>F.leave(e,u({distributionId:e},t)),getStatus:e=>F.getStatus(e),destroy:()=>F.destroy()},timedReleases:{get:e=>K.get(e),enter:(e,t,r)=>K.enter(e,t,u({distributionId:e},r)),leave:(e,t)=>K.leave(e,u({distributionId:e},t)),complete:e=>K.complete(e),status:e=>K.status(e),startMonitoring:(e,t,r)=>K.startMonitoring(e,t,r),stopMonitoring:e=>K.stopMonitoring(e),isMonitoring:e=>K.isMonitoring(e)},appointments:{get:e=>O.get(e),getSlots:(e,t)=>O.getSlots(e,t),getMe:e=>O.getMe(e),book:(e,t,r)=>O.book(e,t,r),cancel:(e,t,r,n)=>O.cancel(e,t,r,n),reschedule:(e,t,r)=>O.reschedule(e,t,r),startMonitoring:(e,t,r)=>O.startMonitoring(e,t,r),stopMonitoring:e=>O.stopMonitoring(e),isMonitoring:e=>O.isMonitoring(e),bumpMonitorGeneration:e=>O.bumpMonitorGeneration(e),destroy:()=>O.destroy()},beacon:Q,use:W,on:ee,destroy:te};const re=!1!==I.sync&&"object"==typeof I.sync?I.sync:{};U=new y({enabled:!1!==I.sync&&!1!==re.enabled,channelName:re.channelName}),A.info("Inter-tab sync initialized",{tabId:U.getTabId(),enabled:U.isEnabled()});const ne={auth:_,challenges:Y,journeys:Z,beacon:Q,appointments:B.appointments,use:W,on:ee,destroy:te};if(await E.rehydrate(),j.autoResume&&E.getSession())try{await H()}catch(ie){A.warn("Auto-resume on init failed; journeys can be resumed manually",{error:ie instanceof Error?ie.message:String(ie)})}return M=k,A.info("SDK initialized successfully"),ne}export{I as init};
@@ -13,3 +13,26 @@ export declare function buildAdmissionProofHeaders(params: {
13
13
  method?: string;
14
14
  nonce?: string;
15
15
  }): Promise<AdmissionProofHeaders | null>;
16
+ /**
17
+ * Read the persisted DPoP-bound refresh token, or null if none is stored or
18
+ * IndexedDB is unavailable.
19
+ */
20
+ export declare function loadRefreshToken(): Promise<string | null>;
21
+ /**
22
+ * Persist the rotated refresh token. Returns false (without throwing) if the
23
+ * write could not be made durable, so the caller can decide whether to continue
24
+ * with an in-memory-only token.
25
+ */
26
+ export declare function persistRefreshToken(token: string): Promise<boolean>;
27
+ /**
28
+ * Remove any persisted refresh token (logout / forced re-auth). Best-effort:
29
+ * swallows errors so logout never fails on a storage hiccup.
30
+ */
31
+ export declare function clearRefreshToken(): Promise<void>;
32
+ /**
33
+ * One-time cleanup of the pre-memory-only build's durable bearer record: older SDKs
34
+ * persisted access/beacon tokens under this key in the refresh store. The current
35
+ * build never writes it, so delete any leftover from an upgraded client to keep the
36
+ * memory-only invariant true at rest. Best-effort.
37
+ */
38
+ export declare function clearLegacySessionTokens(): Promise<void>;
@@ -1 +1 @@
1
- import{generateId as n}from"../core/utils.js";const t="X-Admission-Key-Thumbprint",e="DPoP",r="DPoP-Nonce",o="fanfare-admission-proof",c="keys",i="current";let a=null,u=null;function l(n){const t=new URL(n);return t.search="",t.hash="",t.toString()}async function s(){const n=await p();return n?.thumbprint??null}async function y(o){const c=await p();if(!c)return null;const i=o.method?.toUpperCase()??"POST",a={typ:"dpop+jwt",alg:"ES256",jwk:c.publicJwk},u={htm:i,htu:l(o.url),iat:Math.floor(Date.now()/1e3),jti:n(),...o.nonce?{nonce:o.nonce}:{}},s=`${d(a)}.${d(u)}`,y=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},c.privateKey,(new TextEncoder).encode(s));return{[t]:c.thumbprint,[e]:`${s}.${b(new Uint8Array(y))}`,...o.nonce?{[r]:o.nonce}:{}}}async function p(){if(u)return u;if(a)return await a;a=(async()=>{if("undefined"==typeof window||"undefined"==typeof document||!globalThis.crypto?.subtle)return null;const n=await async function(){if(!("indexedDB"in globalThis))return null;return await new Promise(n=>{try{const t=indexedDB.open(o,1);t.onupgradeneeded=()=>{const n=t.result;n.objectStoreNames.contains(c)||n.createObjectStore(c)},t.onsuccess=()=>n(t.result),t.onerror=()=>n(null),t.onblocked=()=>n(null)}catch{n(null)}})}();if(!n)return null;try{const t=await async function(n){try{const t=await f(n.transaction(c).objectStore(c).get(i));return t?.privateKey&&t.publicKey&&t.publicJwk&&t.thumbprint?t:null}catch{return null}}(n);if(t)return u=t,t;const e=await async function(){const n=await crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign","verify"]),t=await crypto.subtle.exportKey("jwk",n.publicKey);return{privateKey:n.privateKey,publicKey:n.publicKey,publicJwk:t,thumbprint:await w(t)}}(),r=await async function(n,t){try{return await f(n.transaction(c,"readwrite").objectStore(c).put(t,i)),!0}catch{return!1}}(n,e);return r?(u=e,e):null}finally{n.close()}})();try{return await a}finally{a=null}}function f(n){return new Promise((t,e)=>{n.onsuccess=()=>t(n.result),n.onerror=()=>e(n.error??new Error("IndexedDB request failed"))})}async function w(n){const t=JSON.stringify({crv:n.crv,kty:n.kty,x:n.x,y:n.y}),e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(t));return b(new Uint8Array(e))}function d(n){return b((new TextEncoder).encode(JSON.stringify(n)))}function b(n){let t="";for(const e of n)t+=String.fromCharCode(e);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/g,"")}export{t as ADMISSION_KEY_THUMBPRINT_HEADER,e as DPOP_HEADER,r as DPOP_NONCE_HEADER,y as buildAdmissionProofHeaders,l as canonicalizeAdmissionProofUrl,s as getAdmissionKeyThumbprint};
1
+ import{generateId as t}from"../core/utils.js";const n="X-Admission-Key-Thumbprint",e="DPoP",r="DPoP-Nonce",o="keys",a="current",c="refresh",i="current";let u=null,s=null;function l(t){const n=new URL(t);return n.search="",n.hash="",n.toString()}async function y(){const t=await w();return t?.thumbprint??null}async function f(o){const a=await w();if(!a)return null;const c=o.method?.toUpperCase()??"POST",i={typ:"dpop+jwt",alg:"ES256",jwk:a.publicJwk},u={htm:c,htu:l(o.url),iat:Math.floor(Date.now()/1e3),jti:t(),...o.nonce?{nonce:o.nonce}:{}},s=`${K(i)}.${K(u)}`,y=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},a.privateKey,(new TextEncoder).encode(s));return{[n]:a.thumbprint,[e]:`${s}.${k(new Uint8Array(y))}`,...o.nonce?{[r]:o.nonce}:{}}}async function w(){if(s)return s;if(u)return await u;u=(async()=>{if("undefined"==typeof window||"undefined"==typeof document||!globalThis.crypto?.subtle)return null;const t=await g();if(!t)return null;try{const n=await async function(t){try{const n=await S(t.transaction(o).objectStore(o).get(a));return n?.privateKey&&n.publicKey&&n.publicJwk&&n.thumbprint?n:null}catch{return null}}(t);if(n)return s=n,n;const e=await async function(){const t=await crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},!1,["sign","verify"]),n=await crypto.subtle.exportKey("jwk",t.publicKey);return{privateKey:t.privateKey,publicKey:t.publicKey,publicJwk:n,thumbprint:await j(n)}}(),r=await async function(t,n){try{return await S(t.transaction(o,"readwrite").objectStore(o).put(n,a)),!0}catch{return!1}}(t,e);return r?(s=e,e):null}finally{t.close()}})();try{return await u}finally{u=null}}async function p(t){const n=await g();if(n)try{await S(n.transaction(c,"readwrite").objectStore(c).delete(t))}catch{}finally{n.close()}}async function d(){const t=await async function(t){const n=await g();if(!n)return null;try{return await S(n.transaction(c).objectStore(c).get(t))}catch{return null}finally{n.close()}}(i);return"string"==typeof t&&t.length>0?t:null}async function b(t){return async function(t,n){const e=await g();if(!e)return!1;try{return await S(e.transaction(c,"readwrite").objectStore(c).put(n,t)),!0}catch{return!1}finally{e.close()}}(i,t)}async function h(){await p(i)}async function m(){await p("session-tokens")}async function g(){return"indexedDB"in globalThis?await new Promise(t=>{try{const n=indexedDB.open("fanfare-admission-proof",2);n.onupgradeneeded=()=>{const t=n.result;t.objectStoreNames.contains(o)||t.createObjectStore(o),t.objectStoreNames.contains(c)||t.createObjectStore(c)},n.onsuccess=()=>t(n.result),n.onerror=()=>t(null),n.onblocked=()=>t(null)}catch{t(null)}}):null}function S(t){return new Promise((n,e)=>{t.onsuccess=()=>n(t.result),t.onerror=()=>e(t.error??new Error("IndexedDB request failed"))})}async function j(t){const n=JSON.stringify({crv:t.crv,kty:t.kty,x:t.x,y:t.y}),e=await crypto.subtle.digest("SHA-256",(new TextEncoder).encode(n));return k(new Uint8Array(e))}function K(t){return k((new TextEncoder).encode(JSON.stringify(t)))}function k(t){let n="";for(const e of t)n+=String.fromCharCode(e);return btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/g,"")}export{n as ADMISSION_KEY_THUMBPRINT_HEADER,e as DPOP_HEADER,r as DPOP_NONCE_HEADER,f as buildAdmissionProofHeaders,l as canonicalizeAdmissionProofUrl,m as clearLegacySessionTokens,h as clearRefreshToken,y as getAdmissionKeyThumbprint,d as loadRefreshToken,b as persistRefreshToken};
@@ -15,6 +15,7 @@ export interface SDKState {
15
15
  distributionCapabilityIndex: Record<string, DistributionCapabilityBinding>;
16
16
  versionMap: Record<string, number>;
17
17
  setSession: (session: Session | null) => void;
18
+ updateSessionExpiry: (expiresAt: string) => void;
18
19
  setRefreshToken: (token: string | null) => void;
19
20
  setBeaconToken: (token: string | null) => void;
20
21
  addActiveExperience: (experienceId: string, session: ExperienceSession) => void;
@@ -1 +1 @@
1
- import{persist as e,createJSONStorage as i}from"zustand/middleware";import{createStore as n}from"zustand/vanilla";import{isBrowser as t,isStorageAvailable as s}from"../core/utils.js";import{getEventBus as o}from"./events.js";const r=["session","activeJourneys","versionMap"],a=["session"];function c(e){if(!e||"object"!=typeof e)return{};const i=e,n={};for(const t of a)t in i&&(n[t]=i[t]);return n}function p(e){if(!e||"object"!=typeof e)return{};const i=e,n={};for(const t of r)t in i&&(n[t]="versionMap"===t?c(i[t]):i[t]);return n}const l=n()(e((e,i)=>({session:null,refreshToken:null,beaconToken:null,activeExperiences:{},activeJourneys:{},capabilityTokens:{},distributionCapabilityIndex:{},versionMap:{},setSession:n=>{e(e=>({session:n,versionMap:{...e.versionMap,session:(e.versionMap.session||0)+1}})),n&&o().emit("auth:authenticated",{session:n,isNew:!i().session})},setRefreshToken:i=>{e(e=>({refreshToken:i,versionMap:{...e.versionMap,refreshToken:(e.versionMap.refreshToken||0)+1}}))},setBeaconToken:i=>{e({beaconToken:i})},addActiveExperience:(i,n)=>{e(e=>({activeExperiences:{...e.activeExperiences,[i]:n},versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}))},removeActiveExperience:i=>{e(e=>{const{[i]:n,...t}=e.activeExperiences;return{activeExperiences:t,versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}})},updateExperienceSession:(i,n)=>{e(e=>{const t=e.activeExperiences[i];return t?{activeExperiences:{...e.activeExperiences,[i]:{...t,...n}},versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}:e})},clearActiveExperiences:()=>{e(e=>({activeExperiences:{},versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}))},setJourneyState:(i,n)=>{const t={...n,schemaVersion:2};e(e=>({activeJourneys:{...e.activeJourneys,[i]:t}}))},removeJourney:i=>{e(e=>{const{[i]:n,...t}=e.activeJourneys;return{activeJourneys:t}})},clearJourneys:()=>{e(()=>({activeJourneys:{}}))},upsertCapabilityToken:(i,n)=>{e(e=>({capabilityTokens:{...e.capabilityTokens,[i]:n},versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1}}))},bindDistribution:(i,n)=>{e(e=>({distributionCapabilityIndex:{...e.distributionCapabilityIndex,[i]:n},versionMap:{...e.versionMap,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}))},clearCapabilityContext:(i,n)=>{e(e=>{const t=`${i}:${n}`,{[t]:s,...o}=e.capabilityTokens,r={};for(const[i,n]of Object.entries(e.distributionCapabilityIndex))n.sequenceKey!==t&&(r[i]=n);return{capabilityTokens:o,distributionCapabilityIndex:r,versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}})},clearExperienceCapabilityContext:i=>{e(e=>{const n={};for(const[s,o]of Object.entries(e.capabilityTokens))o.experienceId!==i&&(n[s]=o);const t={};for(const[s,o]of Object.entries(e.distributionCapabilityIndex))o.experienceId!==i&&(t[s]=o);return{capabilityTokens:n,distributionCapabilityIndex:t,versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}})},clearExpiredCapabilityTokens:i=>{e(e=>{const n=/* @__PURE__ */new Set,t={};for(const[o,r]of Object.entries(e.capabilityTokens))r.expiresAtMs&&r.expiresAtMs<=i?n.add(o):t[o]=r;if(0===n.size)return e;const s={};for(const[i,o]of Object.entries(e.distributionCapabilityIndex))n.has(o.sequenceKey)||(s[i]=o);return{capabilityTokens:t,distributionCapabilityIndex:s,versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}})},clearAll:()=>{e({session:null,refreshToken:null,beaconToken:null,activeExperiences:{},activeJourneys:{},capabilityTokens:{},distributionCapabilityIndex:{},versionMap:{}}),o().emit("state:cleared",{})}}),{name:"fanfare-sdk-state",storage:i(()=>(()=>{if(t()&&s("localStorage"))return window.localStorage;const e={};return{getItem:i=>Object.prototype.hasOwnProperty.call(e,i)?e[i]:null,setItem:(i,n)=>{e[i]=n},removeItem:i=>{delete e[i]}}})()),partialize:e=>p(e),version:3,merge:(e,i)=>({...i,...p(e)}),onRehydrateStorage:()=>e=>{if(e){const i=e.activeJourneys??{};o().emit("state:restored",{experiences:Object.keys(i)})}}}));function v(){l.setState({session:null,refreshToken:null,beaconToken:null,activeExperiences:{},activeJourneys:{},capabilityTokens:{},distributionCapabilityIndex:{},versionMap:{}}),b=null}let b=null;const u=()=>b||(b=new Proxy({},{get:(e,i)=>Reflect.has(e,i)?e[i]:l.getState()[i],has:(e,i)=>Reflect.has(e,i)||i in l.getState(),ownKeys:e=>{const i=new Set(Reflect.ownKeys(l.getState()));for(const n of Reflect.ownKeys(e))i.add(n);return Array.from(i)},getOwnPropertyDescriptor:(e,i)=>{const n=Object.getOwnPropertyDescriptor(e,i);if(n)return n;const t=Object.getOwnPropertyDescriptor(l.getState(),i);return t?{...t,configurable:!0}:void 0}}),b);export{u as getSDKStore,v as resetSDKStore,l as useSDKStore};
1
+ import{persist as e,createJSONStorage as i}from"zustand/middleware";import{createStore as n}from"zustand/vanilla";import{isBrowser as t,isStorageAvailable as s}from"../core/utils.js";import{getEventBus as o}from"./events.js";const r=["session","activeJourneys","versionMap"],a=["session"];function c(e){if(!e||"object"!=typeof e)return{};const i=e,n={};for(const t of a)t in i&&(n[t]=i[t]);return n}function p(e){if(!e||"object"!=typeof e)return{};const i=e,n={};for(const t of r)t in i&&(n[t]="versionMap"===t?c(i[t]):i[t]);return n}const l=n()(e((e,i)=>({session:null,refreshToken:null,beaconToken:null,activeExperiences:{},activeJourneys:{},capabilityTokens:{},distributionCapabilityIndex:{},versionMap:{},setSession:n=>{e(e=>({session:n,versionMap:{...e.versionMap,session:(e.versionMap.session||0)+1}})),n&&o().emit("auth:authenticated",{session:n,isNew:!i().session})},updateSessionExpiry:i=>{e(e=>e.session?{session:{...e.session,expiresAt:i},versionMap:{...e.versionMap,session:(e.versionMap.session||0)+1}}:{})},setRefreshToken:i=>{e(e=>({refreshToken:i,versionMap:{...e.versionMap,refreshToken:(e.versionMap.refreshToken||0)+1}}))},setBeaconToken:i=>{e({beaconToken:i})},addActiveExperience:(i,n)=>{e(e=>({activeExperiences:{...e.activeExperiences,[i]:n},versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}))},removeActiveExperience:i=>{e(e=>{const{[i]:n,...t}=e.activeExperiences;return{activeExperiences:t,versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}})},updateExperienceSession:(i,n)=>{e(e=>{const t=e.activeExperiences[i];return t?{activeExperiences:{...e.activeExperiences,[i]:{...t,...n}},versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}:e})},clearActiveExperiences:()=>{e(e=>({activeExperiences:{},versionMap:{...e.versionMap,activeExperiences:(e.versionMap.activeExperiences||0)+1}}))},setJourneyState:(i,n)=>{const t={...n,schemaVersion:2};e(e=>({activeJourneys:{...e.activeJourneys,[i]:t}}))},removeJourney:i=>{e(e=>{const{[i]:n,...t}=e.activeJourneys;return{activeJourneys:t}})},clearJourneys:()=>{e(()=>({activeJourneys:{}}))},upsertCapabilityToken:(i,n)=>{e(e=>({capabilityTokens:{...e.capabilityTokens,[i]:n},versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1}}))},bindDistribution:(i,n)=>{e(e=>({distributionCapabilityIndex:{...e.distributionCapabilityIndex,[i]:n},versionMap:{...e.versionMap,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}))},clearCapabilityContext:(i,n)=>{e(e=>{const t=`${i}:${n}`,{[t]:s,...o}=e.capabilityTokens,r={};for(const[i,n]of Object.entries(e.distributionCapabilityIndex))n.sequenceKey!==t&&(r[i]=n);return{capabilityTokens:o,distributionCapabilityIndex:r,versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}})},clearExperienceCapabilityContext:i=>{e(e=>{const n={};for(const[s,o]of Object.entries(e.capabilityTokens))o.experienceId!==i&&(n[s]=o);const t={};for(const[s,o]of Object.entries(e.distributionCapabilityIndex))o.experienceId!==i&&(t[s]=o);return{capabilityTokens:n,distributionCapabilityIndex:t,versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}})},clearExpiredCapabilityTokens:i=>{e(e=>{const n=/* @__PURE__ */new Set,t={};for(const[o,r]of Object.entries(e.capabilityTokens))r.expiresAtMs&&r.expiresAtMs<=i?n.add(o):t[o]=r;if(0===n.size)return e;const s={};for(const[i,o]of Object.entries(e.distributionCapabilityIndex))n.has(o.sequenceKey)||(s[i]=o);return{capabilityTokens:t,distributionCapabilityIndex:s,versionMap:{...e.versionMap,capabilityTokens:(e.versionMap.capabilityTokens||0)+1,distributionCapabilityIndex:(e.versionMap.distributionCapabilityIndex||0)+1}}})},clearAll:()=>{e({session:null,refreshToken:null,beaconToken:null,activeExperiences:{},activeJourneys:{},capabilityTokens:{},distributionCapabilityIndex:{},versionMap:{}}),o().emit("state:cleared",{})}}),{name:"fanfare-sdk-state",storage:i(()=>(()=>{if(t()&&s("localStorage"))return window.localStorage;const e={};return{getItem:i=>Object.prototype.hasOwnProperty.call(e,i)?e[i]:null,setItem:(i,n)=>{e[i]=n},removeItem:i=>{delete e[i]}}})()),partialize:e=>p(e),version:3,merge:(e,i)=>({...i,...p(e)}),onRehydrateStorage:()=>e=>{if(e){const i=e.activeJourneys??{};o().emit("state:restored",{experiences:Object.keys(i)})}}}));function v(){l.setState({session:null,refreshToken:null,beaconToken:null,activeExperiences:{},activeJourneys:{},capabilityTokens:{},distributionCapabilityIndex:{},versionMap:{}}),b=null}let b=null;const u=()=>b||(b=new Proxy({},{get:(e,i)=>Reflect.has(e,i)?e[i]:l.getState()[i],has:(e,i)=>Reflect.has(e,i)||i in l.getState(),ownKeys:e=>{const i=new Set(Reflect.ownKeys(l.getState()));for(const n of Reflect.ownKeys(e))i.add(n);return Array.from(i)},getOwnPropertyDescriptor:(e,i)=>{const n=Object.getOwnPropertyDescriptor(e,i);if(n)return n;const t=Object.getOwnPropertyDescriptor(l.getState(),i);return t?{...t,configurable:!0}:void 0}}),b);export{u as getSDKStore,v as resetSDKStore,l as useSDKStore};
@@ -46,6 +46,14 @@ export interface FanfareConfig {
46
46
  * @default "include"
47
47
  */
48
48
  credentials?: RequestCredentials;
49
+ /**
50
+ * Automatically restore and resume persisted journeys on init (re-registering
51
+ * their capability grants) when a session is present, so a reload mid-experience
52
+ * can act immediately. Disable to manage resume manually via `journeys.resumeAll()`.
53
+ *
54
+ * @default true
55
+ */
56
+ autoResume?: boolean;
49
57
  /**
50
58
  * Enable debug mode
51
59
  * @default false
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * SDK version
3
3
  * This is automatically updated during the build process
4
4
  */
5
- export declare const version = "0.7.2";
5
+ export declare const version = "0.8.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- const o="0.7.2";export{o as version};
1
+ const o="0.8.0";export{o as version};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanfare-io/fanfare-sdk-core",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Official Fanfare Browser SDK for queue, draw, auction, and appointment experiences",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
@@ -122,7 +122,7 @@
122
122
  "uuid": "^14.0.0",
123
123
  "wretch": "^2.11.0",
124
124
  "zustand": "^5.0.6",
125
- "@fanfare-io/fanfare-sdk-contracts": "0.7.2"
125
+ "@fanfare-io/fanfare-sdk-contracts": "0.8.0"
126
126
  },
127
127
  "peerDependencies": {
128
128
  "valibot": "^1.1.0",
@@ -165,7 +165,7 @@
165
165
  "vite-plugin-dts": "^4.5.4",
166
166
  "vite-tsconfig-paths": "^5.1.4",
167
167
  "vitest": "^3.2.4",
168
- "@fanfare-io/shared-models": "0.7.2"
168
+ "@fanfare-io/shared-models": "0.8.0"
169
169
  },
170
170
  "sideEffects": false,
171
171
  "keywords": [
@@ -182,7 +182,7 @@
182
182
  {
183
183
  "name": "Core + Queues",
184
184
  "path": "dist/index.js",
185
- "limit": "42 KB"
185
+ "limit": "43 KB"
186
186
  }
187
187
  ],
188
188
  "scripts": {